Sprites: things that move without the screen moving

Everything drawn on this machine was in a cell. Something between two
cells meant rewriting both; something moving a pixel at a time meant
rewriting them sixty times a second, which is affordable for one thing
and not for twenty. A sprite is put at a pixel and the device draws it
over whatever is behind, so moving it costs two bytes.

MADE OF TILES, which is the decision the rest follows from: m by n taken
in reading order from one index, so there is no second pixel format, no
second kind of memory, and nothing a sprite can show that the map cannot.
A 16 by 16 character is four tiles and the background can name the same
four.

256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry
address is a shift, the same no-multiply argument as the palette's four.
Position is signed and sixteen bits, because 640 by 400 does not fit in a
byte and a sprite has to be able to sit half off the left rather than
appearing whole at the edge.

A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested
before the attribute is added, so a hole belongs to the art and not to
the colour scheme. The same rule the other way round is what "behind"
means: drawn only where the background pixel was zero, so a thing walks
behind a pillar and in front of the floor in one frame.

All of them draw, every frame, so they cannot flicker. Real machines
dropped them per scanline because they had a fixed number of shift
registers; this has a loop. The limit is the size of the table, which is
a constant rather than a property of what is on screen.

And the system takes them down at exit. The sprite table sits in the gap
the screen save walks around - to the end of the map, then the palette -
and that is right, because nothing the shell draws is a sprite: there is
nothing to give back, only something to take away. Otherwise a program
that put a ball up and left would leave it over the prompt, in front of
everything, with nothing able to type it away. Sprite.asm deliberately
leaves its own, because a program that faulted could not have cleared it.

Every check here was re-broken and failed: transparency, reading order,
draw order, priority, and size. Size needed breaking twice - the first
attempt did not compile, and a silent build failure had left the old
binary passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-02 11:42:11 -04:00
co-authored by Claude Opus 5
parent eee95ef0ce
commit a916103a7f
21 changed files with 801 additions and 14 deletions
+247
View File
@@ -0,0 +1,247 @@
; A thing that moves without the screen moving.
;
; Everything drawn on this machine before sprites was in a CELL. Something between two cells
; meant rewriting both of them, and something moving a pixel at a time meant rewriting them
; sixty times a second - which is affordable for one thing and not for twenty.
;
; A sprite is put at a PIXEL, and the device draws it over whatever is already there. Moving
; it costs two bytes: the low and high halves of where it now is. That is the whole of the
; loop below, and it is why this program can run over the shell's own text without disturbing
; a single character of it - nothing underneath is written to at all.
;
; ---- What it leaves behind ----
;
; The sprite, still in the table. On purpose, and for the same reason Flip leaves the screen
; it flipped to: a program that FAULTED would have left it too, and a system that only tidied
; up after programs which remembered would be one that left a ball sitting over the prompt
; the first time somebody's game crashed. The table is the system's to clear.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x5000
start:
; The atlas, which is where both the tiles and the sprite table live. Four is what CosmOS
; uses for it; see the table in the CosmOS README for who owns which number.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8
SETD.0 Message
SWI osPrintString
INIA 0x0A
OUTA 0x00
CALL putBall
CALL screenWidth
CALL putSprite
; Key mode, so a key arrives when it is pressed rather than when Return is.
INIA 0x01
OUTA 0x02
everyFrame:
CALL waitFrame
CALL stepBall
CALL moveSprite
; Anything typed ends it. Asked for and never waited on, so a key pressed between frames
; is still there when this looks.
INA 0x01
INIB 0x01 ; READY
AND
BRQ everyFrame
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
RSTA
OUTA 0x02 ; Line mode again. The sprite is left where it is.
SWI osExit
; ---- The art, into a tile above the font ----
;
; Two hundred, which is well clear of the 135 glyphs the character generator copies back, so
; nothing here costs the shell a letter. Blitted rather than poked: it is already sixty four
; bytes of Data Segment and the controller moves it in one command.
putBall:
INIA 0x01
OUTA 0xE0 ; SourceBank: Data Memory.
; The pointer written down before it is read out of memory a byte at a time, because a Data
; Pointer's two halves cannot be got at any other way.
SETD.1 BallArtAt
SETD.0 BallArt
STD.0.1
LDA.1
OUTA 0xE1
INCD.1
LDA.1
OUTA 0xE2
INIA 0d4
OUTA 0xE3
INIA 0x32
OUTA 0xE4 ; Tile 200 begins at 200 times 64, which is 0x3200.
RSTA
OUTA 0xE5
OUTA 0xE6
INIA 0x40
OUTA 0xE7 ; Sixty four bytes.
INIA 0x01
OUTA 0xE8 ; Blit.
RET
; ---- How wide the screen is, in pixels ----
;
; Columns times eight, and this machine cannot multiply. A and B are one sixteen bit shift
; register though: with the column count in A and nothing in B, A:B holds columns times 256,
; and five shifts right divide that by thirty two - which is columns times eight, high byte
; left in A and low byte in B.
;
; Asked rather than assumed, because the shell runs eighty columns and a game may well have
; asked for forty before starting this.
screenWidth:
INA 0x32
RSTB
SHR
SHR
SHR
SHR
SHR
SETD.0 WidthHigh
STA.0
; B cannot be stored, and there is no move from it. Adding nothing to it puts it in Q,
; which can be copied to A, which can.
RSTA
CCF
ADD
MVQA
SETD.0 WidthLow
STA.0
RET
; ---- The entry, written straight through ----
;
; The controller's Data port steps its address on after every byte, so all eight go out of
; one port with the address named once.
putSprite:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5 ; Sprite nought is at 0xC000.
INIA 0xC8
OUTA 0xE9 ; Tile 200.
INIA 0x01
OUTA 0xE9 ; Attribute one, so the ball comes out in scheme one's ink.
RSTA
OUTA 0xE9
OUTA 0xE9 ; X, low then high.
INIA 0d96
OUTA 0xE9
RSTA
OUTA 0xE9 ; Y, ninety six pixels down.
INIA 0x11
OUTA 0xE9 ; One tile across by one down.
RSTA
OUTA 0xE9 ; Not mirrored, not turned over, not behind.
RET
; A frame, which is the only regular beat this machine has.
waitFrame:
INA 0x30
INIB 0x01
AND
BRQ waitFrame
RET
; ---- One pixel to the right ----
;
; Sixteen bits in two bytes, so the high one is stepped only when the low one came back round
; to nought - which is what a carry is, done by hand.
stepBall:
SETD.0 BallX
LDA.0
INCA
STA.0
BNA stepCheck
SETD.0 BallXHigh
LDA.0
INCA
STA.0
; Round to the left edge at the far side. Both halves have to match, and the high one is
; tested first because it is the one that is usually wrong.
stepCheck:
SETD.0 BallXHigh
LDA.0
SETD.1 WidthHigh
LDB.1
CCF
SUB
BNQ stepDone
SETD.0 BallX
LDA.0
SETD.1 WidthLow
LDB.1
CCF
SUB
BNQ stepDone
RSTA
STA.0 ; DP0 is still BallX, from the comparison just above.
SETD.0 BallXHigh
STA.0
stepDone:
RET
; Two bytes out of one port, which is the whole cost of moving a sprite.
moveSprite:
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
INIA 0x02
OUTA 0xE5 ; X is bytes two and three of the entry.
SETD.0 BallX
LDA.0
OUTA 0xE9
SETD.0 BallXHigh
LDA.0
OUTA 0xE9
RET
#Data
#Base 0x3000
Message:
"A ball, over the shell's own words. Nothing underneath is written to. Press a key."
; Index nought is not a colour, it is a hole - so the corners of the tile are what the ball
; is not, and whatever is behind shows through them.
BallArt:
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
BallArtAt:
#Reserve 0d2
BallX:
0x00
BallXHigh:
0x00
WidthLow:
0x00
WidthHigh:
0x00
+9
View File
@@ -725,6 +725,7 @@ from every assembly file in it. Several are old programs written for the bare ma
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
| Sprite | Moves a ball across the shell's own text, writing not one byte of the map to do it. It leaves the sprite in the table on the way out, because clearing them is the system's job - see below. |
| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. |
| Edit | A line editor. |
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
@@ -1204,6 +1205,14 @@ before quietly answers to nothing.
| 5 | The screen's map, for the same. |
| 6 and up | Free for a program to use. |
**The sprite table is cleared at exit, not saved and restored.** It lives in the atlas at
0xC000, and the pages the screen save walks run to the end of the map and pick up again at the
palette, with the sprites in the gap between. That is deliberate: nothing the shell draws is a
sprite, so there is nothing to give back. What is needed is to take away, or a program that
put something on the screen and left would have left it sitting over the prompt, in front of
everything, with nothing able to type it away. A program that faulted could not have cleared
its own, which is why the system does it for every program rather than trusting each one.
4 and 5 are only registered while a screen is being saved or given back, so a program is free
to point them somewhere else in between - but a program that takes the screen will find them
pointing at the screen again afterwards, so there is nothing to be gained by it. `Grid` uses
+26
View File
@@ -5150,6 +5150,32 @@ screenSane:
; the floor for the programs that saved nothing.
INIA 0x03
OUTA 0x39
; ---- And no sprites left standing over the shell ----
;
; The table is in the atlas at 0xC000, and the screen save does not cover it: the pages it
; walks run to the end of the map and pick up again at the palette, with the sprites in the
; gap between. That is deliberate rather than an oversight. Nothing the shell draws is a
; sprite, so there is nothing to GIVE BACK - what is needed is to take away, or a program
; that put something on the screen and left would have left it sitting over the prompt,
; still there, still in front of everything, and nothing able to type it away.
;
; A size of nought is no sprite, so filling the table with nought is all of it, and it is
; one command rather than 256 of them.
CALL screenBank
INIA 0d4
OUTA 0xE3
INIA 0xC0
OUTA 0xE4
RSTA
OUTA 0xE5
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
INIA 0x08
OUTA 0xE6
RSTA
OUTA 0xE7 ; 0x0800, which is 256 entries of eight bytes.
INIA 0x02
OUTA 0xE8
RET
; Video memory as banks 4 and 5, which is what makes it reachable at all. Bank 3 is the