Files
SplitBit-Emulator/Programs/CosmOS/Apps/Grid.asm
T
AnachronautandClaude Opus 5 bcd42e75ca Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were
missing and this adds them: a column origin so the map can be wider than
the screen as well as taller, and a pixel remainder for each axis so the
step can be one pixel rather than eight.

  0x36  Scroll column, in cells, wrapping at 128
  0x37  Fine X, 0 to 7 pixels
  0x38  Fine Y, 0 to 7 pixels

FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0,
because only its low three bits mean anything. The alternative was for a
write of 8 to step the coarse register, and it was rejected for one reason:
a program that scrolls has to know where it has got to, and if the hardware
carries then the only way to find out is to read the register back. Keeping
them apart means the program already knows, because it did the arithmetic
itself. It is also what the machines this one is pretending to be did.

The renderer now draws one more row and one more column than fit and clips
them, because with a fine offset the screen no longer begins on a cell
boundary and the cells at two edges are partly off it.

videoPutCell follows the column origin as it has always followed the row -
a caller means a cell of the SCREEN, and the screen is a window onto the
map. The fine offsets are deliberately not applied there: they move the
finished picture by less than a cell, and there is no such thing as less
than a cell to write into. So a program may scroll to any pixel without the
console's idea of where row three, column five is moving underneath it.

Grid now scrolls diagonally, a pixel a frame, in four port writes and two
carries. It moved eight pixels every fourth frame before, which reads as
the picture jumping rather than travelling.

Seven checks, each one the same program with one register changed, so what
is compared is where the picture stopped. Breaking fine X, fine Y, the
column origin, the three-bit mask, or the console's use of the origin each
fails exactly one of them.

Grid's own two checks had to be rewritten, and the reason is worth keeping:
they asked whether pixel 4 was a grid line, which was really a check that
the scroll happened to be at a cell boundary. A picture that moves a pixel
a frame can only be asked things that are true at every offset - that it
repeats every eight pixels, and that one band of eight rows holds different
colours from the next.

Also repairs docs.sh, which found the minimal CosmOS application by taking
the first asm block in the README. Documenting a program with an example
above it made that a different block, and the check complained that the
minimal application had no #Base about something that never claimed to be
one. It looks under System Services now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-30 18:52:15 -04:00

426 lines
12 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 ----
;
; A frame, then one pixel down and one across. This used to move a whole cell every fourth
; frame, because a cell was as fine as the screen could be moved - eight pixels at a time,
; which reads as the picture jumping rather than travelling.
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
; ---- A pixel down, and the cell it belongs to ----
;
; Fine is the low three bits of the register and does not carry, so this does: eight steps
; inside the cell and then one step of the origin. The AND is both the wrap and the test -
; Q coming out as nought is exactly the moment the cell boundary was crossed.
SETD.0 FineDown
LDA.0
INCA
INIB 0x07
AND
STQ.0
OUTQ 0x38
BNQ stepAcross
; The map is 128 rows against a screen of 25 or 50, so the origin walks a ring: what leaves
; the top has not gone anywhere and comes back round.
SETD.0 OriginDown
LDA.0
INCA
INIB 0x7F
AND
STQ.0
OUTQ 0x34
stepAcross:
; And the same sideways, which is the axis that did not exist at all until now. There are
; 128 columns against the 80 shown, so this ring is shallower but it is the same ring.
SETD.0 FineAcross
LDA.0
INCA
INIB 0x07
AND
STQ.0
OUTQ 0x37
BNQ everyFrame
SETD.0 OriginAcross
LDA.0
INCA
INIB 0x7F
AND
STQ.0
OUTQ 0x36
BRI everyFrame
finished:
; The key that stopped it, taken so the shell does not find it waiting.
INA 0x00
; ---- Putting the screen back ----
;
; All four scroll registers, or the shell inherits a view that begins half way into a cell.
RSTA
OUTA 0x36
OUTA 0x37
OUTA 0x38
; The row origin too, and 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.
OUTA 0x34 ; A is still nought, from the three above.
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
FineDown:
0x00
FineAcross:
0x00
OriginDown:
0x00
OriginAcross:
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