It said forty and filled exactly half of an eighty column screen. CosmOS asks for the wide mode when it starts, because its own help text is seventy-four characters across. So a program that assumes the shape the MACHINE wakes up in is wrong about the shape the SYSTEM is running in - and the Programming Manual says as much where it describes the columns and rows registers: how big the screen is, is asked for rather than assumed. Port 0x32 says. One instruction, and the program now fills whatever it is given. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
402 lines
11 KiB
NASM
402 lines
11 KiB
NASM
; Grid, the first program to use the screen as a screen.
|
|
;
|
|
; Everything drawn on this machine so far has been text or a bitmap. The tile engine has
|
|
; been there since the screen was built and nothing had touched it: the console uses it,
|
|
; but only ever to put a letter in a cell, which is the one thing it can do that a plain
|
|
; character display could do too.
|
|
;
|
|
; This redefines a tile, fills a map bigger than the screen with it, and then scrolls that
|
|
; map by writing ONE BYTE A FRAME. No memory moves. The rows above and below the screen are
|
|
; already there, so what a scroll costs is not the 2,000 bytes of a screenful but the one
|
|
; byte that says which row is on top.
|
|
;
|
|
; ---- Where its tiles live ----
|
|
;
|
|
; At 200, and the font is why. The machine wakes with the font in tile memory - glyph n at
|
|
; tile n, for 135 of the 256 - so a program that starts writing tiles at zero paints over
|
|
; the alphabet and the shell it is going to hand the machine back to. Above 135 is empty and
|
|
; nobody else's, so nothing here has to be put back afterwards except the map.
|
|
;
|
|
; Written by Anachronaut
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x4000
|
|
|
|
start:
|
|
; ---- Reaching video memory ----
|
|
;
|
|
; The CPU cannot touch it. It belongs to the device, and the only way in is to give it a
|
|
; bank number and go through the memory controller - the same as the disk's buffer.
|
|
INIA 0d3
|
|
OUTA 0xE3 ; DestBank: the number it will answer to.
|
|
INIA 0x30
|
|
OUTA 0xE2 ; SourceLow: the port of the device that owns it.
|
|
INIA 0x03
|
|
OUTA 0xE8 ; RegisterBank.
|
|
|
|
CALL putTile
|
|
CALL putPalette
|
|
CALL putMap
|
|
|
|
; Key mode, so that a key arrives when it is pressed rather than when Return is. Put back
|
|
; before this returns, and CosmOS puts it back too if a program forgets.
|
|
INIA 0x01
|
|
OUTA 0x02
|
|
|
|
; ---- The loop ----
|
|
;
|
|
; Wait for the screen to finish a frame, count four of them, move the origin down one row.
|
|
; That is fifteen rows a second, which is slow enough to watch and fast enough to look
|
|
; deliberate.
|
|
RSTA
|
|
SETD.0 Ticks
|
|
STA.0
|
|
|
|
everyFrame:
|
|
CALL waitFrame
|
|
|
|
; Anything typed ends it. Asked for, never waited for: the console holds the key until
|
|
; somebody wants it, so nothing pressed between frames is lost.
|
|
INA 0x01
|
|
INIB 0x01 ; READY
|
|
AND
|
|
BNQ finished
|
|
|
|
SETD.0 Ticks
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
INIB 0d4
|
|
CCF
|
|
SUB
|
|
BNQ everyFrame
|
|
|
|
RSTA
|
|
STA.0 ; DP0 is still Ticks, from the count just above.
|
|
|
|
; ---- The scroll itself ----
|
|
;
|
|
; One byte. The map is 128 rows and the screen shows 25 of them, so this walks the origin
|
|
; through a ring: what leaves the top has not gone anywhere and comes back round.
|
|
SETD.0 Origin
|
|
LDA.0
|
|
INCA
|
|
INIB 0x7F
|
|
AND
|
|
MVQA
|
|
STA.0
|
|
OUTA 0x34
|
|
BRI everyFrame
|
|
|
|
finished:
|
|
; The key that stopped it, taken so the shell does not find it waiting.
|
|
INA 0x00
|
|
|
|
; ---- Putting the screen back ----
|
|
;
|
|
; The origin first, then every cell of the map and not just the visible ones. A console
|
|
; that scrolls would otherwise walk down into rows this program filled, and find a grid
|
|
; underneath its own output.
|
|
RSTA
|
|
OUTA 0x34
|
|
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
INIA 0x40
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE2 ; The byte to write: tile 0 is the space, attribute 0 is plain.
|
|
INIA 0x80
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7 ; 0x8000 bytes, which is the whole map.
|
|
INIA 0x02
|
|
OUTA 0xE8 ; Fill.
|
|
|
|
; ---- And the colours it woke up with ----
|
|
;
|
|
; Only the first pair, and that is worth being honest about. The console's own scheme is
|
|
; sixteen banks - colours on black in 0 to 7, the same colours inverted in 8 to 15 - and
|
|
; this program wrote over all of them, because the attribute nibble lands on exactly the
|
|
; entries the console uses and there is nowhere else for it to land. Putting all thirty two
|
|
; back would mean copying the device's own table into an application, which is the kind of
|
|
; duplication that goes stale the first time somebody picks a nicer green.
|
|
;
|
|
; So it restores bank 0, grey on black, which is what plain text has always been and what
|
|
; the shell will be using when it gets the machine back. The other fifteen keep this
|
|
; program's colours until something else writes them, which is visible only to a program
|
|
; that sets the console's attribute port.
|
|
;
|
|
; THE REAL ANSWER IS A COMMAND TO THE SCREEN saying "give me back what you woke up with",
|
|
; the same way the console has one for clearing. There is not one, and this program is the
|
|
; first thing that ever wanted it.
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
INIA 0xFC
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE9 ; Entry 0, the paper: black.
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
INIA 0xD8
|
|
OUTA 0xE9 ; Entry 1, the ink: grey.
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9
|
|
|
|
OUTA 0x02 ; Line mode, the way it was found.
|
|
RSTA ; splitlint[redundant-assignment]: an exit status, not a mode
|
|
SWI osExit
|
|
|
|
; ---- A frame ----
|
|
;
|
|
; Asked for rather than waited on with an interrupt. Polling costs a program nothing it
|
|
; needs here and saves installing a vector, and the status bit is honest: reading it is what
|
|
; answers it, so this cannot see the same frame twice.
|
|
waitFrame:
|
|
INA 0x30
|
|
INIB 0x01
|
|
AND
|
|
BRQ waitFrame
|
|
RET
|
|
|
|
; ---- The tile ----
|
|
;
|
|
; Blitted rather than written a byte at a time, because it is already sixty four bytes of
|
|
; Data Segment and the controller will move it in one command. Tile n starts at n times 64,
|
|
; so tile 200 starts at 12,800, which is 0x3200.
|
|
putTile:
|
|
INIA 0x01
|
|
OUTA 0xE0 ; SourceBank: Data Memory.
|
|
; The pointer BEFORE storing through it. Written the other way round the first time, which
|
|
; assembles perfectly and stores the address into wherever DP1 was last left - so the blit
|
|
; read its sixty four bytes from nowhere in particular and tile 200 came out as noise.
|
|
SETD.1 TileArtAt
|
|
SETD.0 TileArt
|
|
STD.0.1
|
|
LDA.1
|
|
OUTA 0xE1
|
|
INCD.1
|
|
LDA.1
|
|
OUTA 0xE2
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
INIA 0x32
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE6
|
|
INIA 0d64
|
|
OUTA 0xE7
|
|
INIA 0x01
|
|
OUTA 0xE8 ; Blit.
|
|
RET
|
|
|
|
; ---- The colours ----
|
|
;
|
|
; The tile is drawn in indices 0 and 1, and the low nibble of a cell's attribute is ADDED to
|
|
; every index in it, sixteen at a time. So the same sixty four bytes appear in sixteen colour
|
|
; schemes, and what this writes is those schemes: entry 16n is the ground and 16n+1 the line.
|
|
; Nothing is duplicated to get them.
|
|
;
|
|
; ---- Nothing here works out an address ----
|
|
;
|
|
; The first try computed where entry 16n lives - 0xFC00 plus 64n - and was wrong twice for
|
|
; the same reason, which is that this machine cannot multiply and pretending otherwise is
|
|
; where the bugs go. 64n reaches 960, so the address spans four pages and the high byte moves
|
|
; too; and doubling A by adding B to it needs B to hold A first, which RSTB is the opposite
|
|
; of.
|
|
;
|
|
; So it writes all 256 entries in order and never computes anything. The controller's Data
|
|
; port steps the address on after every byte, so the whole palette is one sweep of 1,024
|
|
; writes with no arithmetic in it at all. The colours that change do so by adding sixteen to
|
|
; a running value, which is the same reason.
|
|
putPalette:
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
INIA 0xFC
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
|
|
SETD.0 Scheme
|
|
STA.0
|
|
SETD.0 LineRed
|
|
STA.0
|
|
INIA 0xFF
|
|
SETD.0 LineGreen
|
|
STA.0
|
|
|
|
everyScheme:
|
|
; Entry 16n, the ground: the same dark under every scheme.
|
|
INIA 0d16
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
INIA 0d24
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9
|
|
|
|
; Entry 16n+1, the line: more red and less green the further down the map it is, so that
|
|
; scrolling is visibly going somewhere rather than showing the same row again.
|
|
SETD.0 LineRed
|
|
LDA.0
|
|
OUTA 0xE9
|
|
SETD.0 LineGreen
|
|
LDA.0
|
|
OUTA 0xE9
|
|
INIA 0d96
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9
|
|
|
|
; The fourteen this tile never asks for. Written anyway, because the sweep is what keeps
|
|
; the address right and skipping them would mean working one out.
|
|
INIA 0d14
|
|
SETD.0 Spare
|
|
STA.0
|
|
everySpare:
|
|
RSTA
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
SETD.0 Spare
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA everySpare
|
|
|
|
SETD.0 LineRed
|
|
LDA.0
|
|
INIB 0d16
|
|
CCF
|
|
ADD
|
|
STQ.0
|
|
SETD.0 LineGreen
|
|
LDA.0
|
|
CCF
|
|
SUB ; B is still sixteen, from the red just above.
|
|
STQ.0
|
|
|
|
SETD.0 Scheme
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
CCF
|
|
SUB ; And still sixteen here, which is also how many schemes there are.
|
|
BNQ everyScheme
|
|
RET
|
|
|
|
; ---- The map ----
|
|
;
|
|
; All 128 rows, not the 25 the screen shows. That is the whole point of a map bigger than
|
|
; the screen: the rows above and below are already drawn, so scrolling is a change of origin
|
|
; rather than a change of anything.
|
|
;
|
|
; A ROW IS A PAGE, which is why the row number is written straight into DestHigh and the
|
|
; column arithmetic disappears. The controller's Data port steps the address on after every
|
|
; byte, so a row is a loop over two writes with no address handling in it at all.
|
|
putMap:
|
|
RSTA
|
|
SETD.0 MapRow
|
|
STA.0
|
|
|
|
everyRow:
|
|
INIA 0d3
|
|
OUTA 0xE3
|
|
SETD.0 MapRow
|
|
LDA.0
|
|
INIB 0x40
|
|
CCF
|
|
ADD
|
|
OUTQ 0xE4 ; The map starts at 0x4000 and a row is a page.
|
|
RSTA
|
|
OUTA 0xE5
|
|
|
|
; The attribute is the row number's low nibble, so the schemes band down the map and
|
|
; repeat every sixteen rows. DP0 is still MapRow, from the row's address above.
|
|
LDA.0
|
|
INIB 0x0F
|
|
AND
|
|
SETD.0 RowAttribute
|
|
STQ.0
|
|
|
|
; ---- How wide the screen is, asked rather than assumed ----
|
|
;
|
|
; This said forty, and filled exactly half of an eighty column screen. CosmOS asks for the
|
|
; wide mode when it starts, because its own help text is seventy-four characters across -
|
|
; so a program that assumes the shape the MACHINE wakes up in is wrong about the shape the
|
|
; SYSTEM is running in. The screen will say if it is asked.
|
|
INA 0x32
|
|
SETD.0 RowCells
|
|
STA.0
|
|
|
|
everyCell:
|
|
INIA 0d200
|
|
OUTA 0xE9
|
|
SETD.0 RowAttribute
|
|
LDA.0
|
|
OUTA 0xE9
|
|
SETD.0 RowCells
|
|
LDA.0
|
|
DECA
|
|
STA.0
|
|
BNA everyCell
|
|
|
|
SETD.0 MapRow
|
|
LDA.0
|
|
INCA
|
|
STA.0
|
|
INIB 0x80
|
|
CCF
|
|
SUB
|
|
BNQ everyRow
|
|
RET
|
|
|
|
#Data
|
|
|
|
#Base 0x2000
|
|
|
|
Ticks:
|
|
0x00
|
|
Origin:
|
|
0x00
|
|
Scheme:
|
|
0x00
|
|
Spare:
|
|
0x00
|
|
LineRed:
|
|
0x00
|
|
LineGreen:
|
|
0x00
|
|
MapRow:
|
|
0x00
|
|
RowAttribute:
|
|
0x00
|
|
RowCells:
|
|
0x00
|
|
TileArtAt:
|
|
0x00 0x00
|
|
|
|
; ---- Eight by eight, a byte a pixel ----
|
|
;
|
|
; A line along the top and one down the left. Tiled edge to edge they meet, so a screenful
|
|
; of this one tile is a continuous grid rather than 1,000 separate boxes.
|
|
TileArt:
|
|
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
|
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|