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
+105 -1
View File
@@ -17,6 +17,18 @@
static uint8_t videoAtlas[VIDEO_MEMORY_BYTES];
static uint8_t videoScreen[VIDEO_SCREEN_COUNT][VIDEO_MEMORY_BYTES];
// ---- Where the background was empty ----
//
// One byte a pixel, set while the map or the bitmap is drawn and read while the sprites are.
// A sprite marked "behind" needs to know whether the thing already at a pixel was a picture
// or a gap, and by the time it is drawn the pixel holds a colour rather than the index it
// came from - the palette is not one to one, so two different indices can be the same
// colour and asking the picture would get it wrong.
//
// Host memory, and it costs the machine nothing: it is scratch the device uses inside one
// frame, exactly like the pixel buffer beside it.
static uint8_t backgroundEmpty[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT];
// Which screen is being shown. The console draws into THIS one rather than into a screen of
// its own, so text goes where whoever is looking is looking - which matters most when the
// text is a fault message printed over a game that had flipped.
@@ -346,6 +358,81 @@ uint8_t videoRead(uint8_t port) {
}
}
// ---- The sprites, over whatever is already there ----
//
// BACKWARDS THROUGH THE TABLE, so that where two overlap the lower number comes out on top:
// it is drawn last and writes over. Every entry is looked at, because the ones that draw
// nothing say so in a byte and skipping them costs one test.
static void drawSprites(uint8_t *pixels, int width, int height) {
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
for (int n = VIDEO_SPRITE_COUNT - 1; n >= 0; n--) {
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
const int wide = (entry[VIDEO_SPRITE_SIZE] >> 4) & 0x0F;
const int tall = entry[VIDEO_SPRITE_SIZE] & 0x0F;
if (wide == 0 || tall == 0) {
continue;
}
// Signed, and low byte first like everything else this machine writes to a device.
const int left = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_X]
| (entry[VIDEO_SPRITE_X + 1] << 8));
const int top = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_Y]
| (entry[VIDEO_SPRITE_Y + 1] << 8));
const uint8_t bank = (uint8_t)((entry[VIDEO_SPRITE_ATTRIBUTE] & 0x0F) << 4);
const uint8_t flags = entry[VIDEO_SPRITE_FLAGS];
const int mirrored = (flags & VIDEO_SPRITE_HFLIP) != 0;
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
for (int downTile = 0; downTile < tall; downTile++) {
for (int acrossTile = 0; acrossTile < wide; acrossTile++) {
// ---- Flipping moves the tiles as well as the pixels ----
//
// A mirrored sprite is not each of its tiles mirrored in place; the tile at
// the left end has to come out at the right end too, or a thing made of more
// than one tile turns inside out instead of round.
const int readAcross = mirrored ? (wide - 1 - acrossTile) : acrossTile;
const int readDown = inverted ? (tall - 1 - downTile) : downTile;
// In reading order from the first, and wrapping, because a byte plus a byte
// is a byte and the tile number is one.
const uint8_t tile = (uint8_t)(entry[VIDEO_SPRITE_TILE]
+ readDown * wide + readAcross);
const uint8_t *art = videoAtlas + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
const int atY = top + downTile * VIDEO_CELL_PIXELS + y;
if (atY < 0 || atY >= height) {
continue;
}
const int fromY = inverted ? (VIDEO_CELL_PIXELS - 1 - y) : y;
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
const int atX = left + acrossTile * VIDEO_CELL_PIXELS + x;
if (atX < 0 || atX >= width) {
continue;
}
const int fromX = mirrored ? (VIDEO_CELL_PIXELS - 1 - x) : x;
const uint8_t pixel = art[fromY * VIDEO_CELL_PIXELS + fromX];
// Nought is not a colour here, it is the absence of one, and it is
// tested before the attribute is added so that it stays the same
// hole in all sixteen schemes.
if (pixel == 0) {
continue;
}
if (behind && !backgroundEmpty[atY * width + atX]) {
continue;
}
const uint8_t index = (uint8_t)(pixel + bank);
const uint8_t *colour = palette + index * VIDEO_PALETTE_BYTES;
uint8_t *out = pixels + (atY * width + atX) * 3;
out[0] = colour[0];
out[1] = colour[1];
out[2] = colour[2];
}
}
}
}
}
}
void videoRender(void) {
if (mode == VIDEO_MODE_BITMAP) {
// ---- A byte a pixel, and nothing in the way ----
@@ -361,9 +448,14 @@ void videoRender(void) {
*out++ = entry[0];
*out++ = entry[1];
*out++ = entry[2];
backgroundEmpty[at] = (from[at] == 0);
}
renderedWidth = VIDEO_BITMAP_WIDTH;
renderedHeight = VIDEO_BITMAP_HEIGHT;
// Over a picture as much as over a map. A bitmap is what a program draws once and
// leaves; sprites are what moves on top of it, and there is no reason the mode that
// cannot afford to redraw itself should be the one that cannot have them.
drawSprites(pixels, VIDEO_BITMAP_WIDTH, VIDEO_BITMAP_HEIGHT);
return;
}
@@ -428,17 +520,29 @@ void videoRender(void) {
// Wrapping, because a byte plus a byte is a byte. A tile using the
// high end of the palette with a nibble set comes round the bottom,
// which is what an adder does and what the manual says it does.
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
const uint8_t was = art[y * VIDEO_CELL_PIXELS + x];
const uint8_t index = (uint8_t)(was + bank);
const uint8_t *entry = videoAtlas + VIDEO_PALETTE_BASE
+ index * VIDEO_PALETTE_BYTES;
uint8_t *out = pixels + (atY * width + atX) * 3;
out[0] = entry[0];
out[1] = entry[1];
out[2] = entry[2];
// Before the nibble, so that a cell drawn in scheme five is empty in the
// same places as the same cell drawn in scheme nought.
backgroundEmpty[atY * width + atX] = (was == 0);
}
}
}
}
// ---- And then the things that move ----
//
// After the map and not woven into it, because a sprite is not tied to a cell: one can
// sit across four of them, and a pass that drew each cell and then whatever overlapped it
// would have to draw parts of the same sprite four times and get the order right between
// them. Over the finished picture there is no order to get wrong.
drawSprites(pixels, width, height);
renderedWidth = width;
renderedHeight = rows * VIDEO_CELL_PIXELS;
}
+75
View File
@@ -89,6 +89,81 @@
#define VIDEO_BITMAP_WIDTH 320
#define VIDEO_BITMAP_HEIGHT 200
// ---- Sprites ----
//
// Things that move without the map moving. A map cell is where it is, and a program that
// wanted something between two cells had to redraw both of them; a sprite is put at a PIXEL
// and the device draws it over whatever is behind.
//
// MADE OF TILES, which is the decision the rest follows from. A sprite is m by n tiles taken
// in reading order from one index, so it needs no pixel format of its own, no second kind of
// memory, and no way for its art to be anything the map could not also show. A 16 by 16
// character is four tiles and a program that wants it in the background too just names the
// same four.
//
// The table is 256 entries of 8 bytes. Eight so that entry n begins at n times eight, which
// is a shift - the same no-multiply argument that makes a palette entry four bytes and a map
// row a page. It sits above the tiles with the whole of 0x4000 to 0xBFFF still clear beneath
// it, which is two more 16K pages of tiles if they are ever wanted.
#define VIDEO_SPRITE_BASE 0xC000
#define VIDEO_SPRITE_COUNT 256
#define VIDEO_SPRITE_BYTES 8
// Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute
// means: its low nibble times sixteen is added to every index in the art.
#define VIDEO_SPRITE_TILE 0
#define VIDEO_SPRITE_ATTRIBUTE 1
// Bytes 2 to 5, low byte first, and SIGNED - a screen is 640 by 400 in the larger mode, so
// neither axis fits in a byte, and a sprite has to be able to sit half off the left or the
// top rather than appearing whole at the edge.
#define VIDEO_SPRITE_X 2
#define VIDEO_SPRITE_Y 4
// Byte 6: how many tiles across in the high nibble, how many down in the low. Fifteen each
// way, so 120 by 120 pixels.
//
// A SPRITE OF NO WIDTH OR NO HEIGHT DRAWS NOTHING, and that is the off switch. It saves a
// flag, it is per sprite rather than a global the whole table shares, and it means the table
// is already off when the machine starts, since the atlas wakes up cleared.
//
// Deliberately the OPPOSITE of what a length of zero means to the memory controller, where
// it means the whole 64K. The reason is the same both times: moving no bytes is a useless
// thing to ask for, so zero was free to mean something else there - and drawing no sprite is
// the commonest state in this table, so zero has to mean nothing here.
#define VIDEO_SPRITE_SIZE 6
// Byte 7.
#define VIDEO_SPRITE_FLAGS 7
#define VIDEO_SPRITE_HFLIP 0x01
#define VIDEO_SPRITE_VFLIP 0x02
// Drawn only where the background had nothing, so a thing can walk behind a pillar. See
// below for what "nothing" means.
#define VIDEO_SPRITE_BEHIND 0x04
// ---- What a sprite does not cover ----
//
// A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no
// other candidate: the font already uses index 0 for paper, so it is the value art in this
// machine has always left empty.
//
// Tested BEFORE the attribute is added, so it is a property of the art and not of the colour
// scheme it is being shown in. A sprite drawn in indices 1 to 15 is transparent in the same
// places in all sixteen schemes, which is the whole point of the additive nibble.
//
// The same rule read the other way is what "behind" means: a sprite marked behind draws only
// where the BACKGROUND pixel was zero. One rule, applied to whichever layer is in front.
//
// ---- How many at once ----
//
// All of them. Every entry in the table is drawn every frame, so sprites cannot flicker.
// Real machines dropped them per scanline because they had a fixed number of shift registers
// and a fixed time to fill them; this has a loop. The limit is how many entries there are,
// which is a constant a program can count on rather than a property of what it happens to be
// drawing this frame.
//
// Where they overlap, THE LOWER NUMBER IS IN FRONT.
// ---- The palette ----
//
// Four bytes an entry rather than three, for the same reason a map row is a page: entry n
+35 -2
View File
@@ -622,7 +622,9 @@ Two rather than one because the two halves of a screen are written at completely
| Bank | Address | Holds |
| --- | --- | --- |
| Atlas | 0x0000 - 0x3FFF | Tile memory. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
| Atlas | 0x4000 - 0xFBFF | Free. |
| Atlas | 0x4000 - 0xBFFF | Free. Two more 16K pages of tiles would fit here. |
| Atlas | 0xC000 - 0xC7FF | The sprite table. 256 entries of eight bytes. |
| Atlas | 0xC800 - 0xFBFF | Free. |
| Atlas | 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused. |
| Screen | 0x0000 - 0x3FFF | Free in a tile mode. |
| Screen | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
@@ -672,13 +674,44 @@ The base port owns the atlas because tiles have been at 0x0000 since there was a
A palette entry is four bytes for the same reason. Entry n begins at n times four, which is a shift; three bytes would need a multiply.
### Sprites:
Things that move without the map moving. A cell is where it is; 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 behind.
**A sprite is m by n tiles**, taken in reading order from one index. That is the decision the rest follows from: it needs no pixel format of its own, no second kind of memory, and nothing its art can be that the map could not also show. A 16 by 16 character is four tiles, and a program that wants the same picture in the background just names the same four.
The table is 256 entries of 8 bytes at **0xC000 in the atlas**. Eight so that entry n begins at n times eight, which is a shift - the same reason a palette entry is four bytes.
| Byte | Holds |
| --- | --- |
| 0 | The top left tile. The rest follow it in reading order, wrapping at 255. |
| 1 | Attribute. Its low nibble times sixteen is added to every index in the art, exactly as a cell's is. |
| 2, 3 | X, low byte first, **signed**. |
| 4, 5 | Y, the same. |
| 6 | Size: tiles across in the high nibble, tiles down in the low. |
| 7 | Flags. Bit 0 mirrors it, bit 1 turns it over, bit 2 puts it behind. |
The position is signed and sixteen bits because the larger mode is 640 by 400, so neither axis fits in a byte - and because a sprite has to be able to sit half off the left or the top rather than appearing whole at the edge.
**A pixel of zero is not drawn.** Without that every sprite is a rectangle. It is tested before the attribute is added, so a hole is a property of the art rather than of the colour scheme: a sprite drawn in indices 1 to 15 is transparent in the same places in all sixteen.
The same rule read the other way is what **behind** means. A sprite marked behind draws only where the background pixel was zero, so a thing can walk behind a pillar and in front of the floor in the same frame. One rule, applied to whichever layer is in front.
**A sprite of no width or no height draws nothing**, and that is the off switch: it saves a flag, it is per sprite rather than a global the whole table shares, and it means the table is already off when the machine starts, since the atlas wakes up cleared. Note that this is deliberately the opposite of what a length of zero means to the memory controller. The reason is the same both times - moving no bytes is a useless thing to ask for, so zero was free to mean 64K there, and drawing no sprite is the commonest state in this table, so zero has to mean nothing here.
**All of them are drawn, every frame.** Sprites here cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers and a fixed time to fill them; this has a loop. The limit is how many entries the table has, which is a constant a program can count on rather than something that depends on what it happens to be drawing. Where two overlap, **the lower number is in front**.
Sprites are drawn over a bitmap as readily as over a map. A bitmap is what a program draws once and leaves; there is no reason the mode that cannot afford to redraw itself should be the one that cannot have things moving on it.
`Programs/CosmOS/Apps/Sprite.asm` moves one across the shell's own text without writing a byte of the map.
### Cells:
Two bytes. The first says which tile, the second how to colour it.
The low nibble of the second byte is **added to every palette index in the tile, sixteen at a time**. A tile drawn in indices 0 to 15 therefore appears in any of sixteen colour schemes without a second copy of it in tile memory. A tile that wants all 256 colours leaves the nibble at zero and gets them. The addition wraps, because a byte plus a byte is a byte.
The high nibble is reserved and should be left at zero, so that a meaning can be given to it later without changing what already-written programs mean.
The high nibble is reserved and should be left at zero, so that a meaning can be given to it later without changing what already-written programs mean. The meaning waiting for it is a tile page: there is room in the atlas for two more pages of 256, and one bit of that nibble would reach them.
### Registers:
+1 -1
View File
@@ -79,7 +79,7 @@ from `make`, not from here.
### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 204 tests, of which 142 run, 35
everything it printed against a file in `Tests/expected`. 205 tests, of which 143 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all.
+2 -1
View File
@@ -23,6 +23,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -39,7 +40,7 @@ outer.script 376
inner.script 44
loop.script 35
crossed.txt 560
26 files, 1 directory
27 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -13,6 +13,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -28,7 +29,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> drive 1
> dir
other.txt 28
+2 -1
View File
@@ -30,6 +30,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -45,7 +46,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -20,6 +20,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -35,7 +36,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -20,6 +20,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -35,7 +36,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -106,6 +106,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -121,7 +122,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -25,6 +25,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -40,7 +41,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -13,6 +13,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -28,7 +29,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> load
load what?
> load nosuch.sbx
+2 -1
View File
@@ -11,6 +11,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -26,7 +27,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> load Say.sbx
loaded, starting at 5000
> run the disk took its time
+43
View File
@@ -0,0 +1,43 @@
CosmOS
> Say before Sprite
it says: before Sprite
finished
> Sprite
A ball, over the shell's own words. Nothing underneath is written to. Press a key.
finished
> Say after Sprite
it says: after Sprite
finished
> dir
greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
notes.txt 21
Apps <dir>
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
26 files, 1 directory
> exit
halted
Execution halted.
[exit 0]
Binary file not shown.
+5
View File
@@ -130,6 +130,11 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Flip.asm" -o "$WORK/Flip.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Flip.sbx" >/dev/null
# Sprite.sbx moves a ball across the shell's own text without writing a byte of the map, and
# leaves the sprite in the table on the way out - so it is what says the system clears them.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Sprite.asm" -o "$WORK/Sprite.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Sprite.sbx" >/dev/null
# Crash.sbx breaks in each of the four ways the system now catches, which is the only way to
# reach a fault screen from a recorded test.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
+13
View File
@@ -977,6 +977,19 @@ cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGri
# osTakeScreen, which restores the screen as it was BEFORE the line was printed and so wiped
# out the one thing it was pointing at. A program that damages nothing should not ask.
cosmosFlip | CosmOS/Source/cosmos.asm | run | cosmosFlip.in | 60000000 | disks/cosmos.img
# ---- A thing that moves without the screen moving ----
#
# Sprite draws a ball over the shell's own text and writes not one byte of the map to do it,
# which is what a sprite is for. The picture is video.sh's business; what is checked here is
# that the words either side of it are untouched - the line printed before it ran and the one
# printed after are both still readable, because nothing underneath the ball was ever written.
#
# A KEYBOARD FIXTURE and not standard input, which is the difference between a test that
# runs this program and one that only looks like it does. A null on standard input is a
# character like any other, so the first of them satisfied the "has a key arrived" test and
# the ball never moved at all - the whole 150 frames of it went past in the shell, reading a
# line made of nulls. Through a keyboard a null is silence, which is what makes it a wait.
cosmosSprite | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | cosmosSprite.keys
# Which disk the registers mean. Several disks are one controller with a drive register
# rather than several devices, because a port is an immediate byte inside the instruction
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
+224 -1
View File
@@ -100,13 +100,26 @@ print(counts.most_common(1)[0][0].hex())
" "$1" 2>/dev/null || echo none
}
# How many pixels of one exact colour a picture has. What a sprite is counted by: it is a
# shape rather than a screenful, so the commonest colour says nothing about it.
countColour() {
# countColour <ppm> <rrggbb>
python3 -c "
import sys
d = open(sys.argv[1], 'rb').read()
px = d[d.index(b'255\n') + 4:]
want = bytes.fromhex(sys.argv[2])
print(sum(1 for o in range(0, len(px), 3) if px[o:o + 3] == want))
" "$1" "$2" 2>/dev/null || echo -1
}
pokeTo() {
# pokeTo <bank> <address> <byte>
printf ' INIA 0d%d\n OUTA 0xE3\n INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n OUTA 0xE9\n' \
"$1" $(( ($2 >> 8) & 0xFF )) $(( $2 & 0xFF )) $(( $3 & 0xFF ))
}
pokeAtlas() { pokeTo 3 "$1" "$2"; } # Tiles and the palette.
pokeAtlas() { pokeTo 3 "$1" "$2"; } # Tiles, the palette and the sprite table.
pokeScreen() { pokeTo 4 "$1" "$2"; } # The map, or a bitmap.
pokeBack() { pokeTo 5 "$1" "$2"; } # The same, in the screen not being shown.
@@ -121,6 +134,48 @@ pokeAtlasRun() {
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
}
# One entry of the sprite table, which is eight bytes at 0xC000 plus eight times its number.
# X and Y are signed and go in low byte first, so a negative one is written as its two's
# complement here rather than being worked out at every call.
spriteAt() {
# spriteAt <n> <tile> <attribute> <x> <y> <size> <flags>
local base=$(( 0xC000 + $1 * 8 ))
local x=$(( $4 & 0xFFFF ))
local y=$(( $5 & 0xFFFF ))
pokeAtlas "$base" "$2"
pokeAtlas "$(( base + 1 ))" "$3"
pokeAtlas "$(( base + 2 ))" "$(( x & 0xFF ))"
pokeAtlas "$(( base + 3 ))" "$(( (x >> 8) & 0xFF ))"
pokeAtlas "$(( base + 4 ))" "$(( y & 0xFF ))"
pokeAtlas "$(( base + 5 ))" "$(( (y >> 8) & 0xFF ))"
pokeAtlas "$(( base + 6 ))" "$6"
pokeAtlas "$(( base + 7 ))" "$7"
}
# Every pixel of one tile the same index. Tile n begins at n times 64.
solidTile() {
# solidTile <tile> <index>
pokeAtlasRun "$(( $1 * 64 ))" "$2" 64
}
# The top half one index and the bottom half another, which is how a tile gets a hole in it:
# index nought is what a sprite does not draw.
halfTile() {
# halfTile <tile> <top> <bottom>
pokeAtlasRun "$(( $1 * 64 ))" "$2" 32
pokeAtlasRun "$(( $1 * 64 + 32 ))" "$3" 32
}
# Four colours to tell tiles apart by, and the pair a background cell in scheme one uses.
spriteColours() {
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 # 1 red
pokeAtlas 0xFC08 0x00; pokeAtlas 0xFC09 0xFF; pokeAtlas 0xFC0A 0x00 # 2 green
pokeAtlas 0xFC0C 0x00; pokeAtlas 0xFC0D 0x00; pokeAtlas 0xFC0E 0xFF # 3 blue
pokeAtlas 0xFC10 0xFF; pokeAtlas 0xFC11 0xFF; pokeAtlas 0xFC12 0x00 # 4 yellow
pokeAtlas 0xFC40 0x00; pokeAtlas 0xFC41 0x00; pokeAtlas 0xFC42 0x00 # 16 black paper
pokeAtlas 0xFC44 0x00; pokeAtlas 0xFC45 0xFF; pokeAtlas 0xFC46 0xFF # 17 cyan ink
}
port() {
# port <port> <byte>
printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF ))
@@ -344,6 +399,145 @@ echo "Checking what the video device draws."
&& result ok "the map is a ring" "row 0 follows row 127" \
|| result no "the map is a ring" "got $(pixel ring 0 8)"
# ---- Sprites ----
#
# A thing put at a PIXEL rather than in a cell. Tile 1 is solid index one, which the palette
# above makes red, and the sprite sits exactly over the cell at row 3 column 2 - so where it
# is can be checked against where it is not, which is the way a tile engine is usually wrong.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x11 0x00
epilogue
} | run sprite || exit 1
[ "$(pixel sprite 16 24)" = "255,0,0" ] && [ "$(pixel sprite 23 31)" = "255,0,0" ] \
&& result ok "a sprite lands where it is put" "and fills its whole eight by eight" \
|| result no "a sprite lands where it is put" "corner $(pixel sprite 16 24), far $(pixel sprite 23 31)"
[ "$(pixel sprite 15 24)" = "0,0,0" ] && [ "$(pixel sprite 24 24)" = "0,0,0" ] \
&& result ok "and stops at its own edge" "a pixel either side is background" \
|| result no "and stops at its own edge" "left $(pixel sprite 15 24), right $(pixel sprite 24 24)"
# ---- What it does not cover ----
#
# Index nought is a hole and not a colour. The tile is solid on top and empty underneath, and
# the cell behind it is cyan, so the bottom half of the sprite must show the cell.
{ prologue
spriteColours
halfTile 3 0x01 0x00
pokeScreen 0x4304 0x01; pokeScreen 0x4305 0x01 # Row 3, column 2: tile 1 in scheme 1.
solidTile 1 0x01
spriteAt 0 3 0x00 16 24 0x11 0x00
epilogue
} | run spritehole || exit 1
[ "$(pixel spritehole 16 24)" = "255,0,0" ] && [ "$(pixel spritehole 16 28)" = "0,255,255" ] \
&& result ok "a pixel of nought is not drawn" "the cell behind shows through the hole" \
|| result no "a pixel of nought is not drawn" "top $(pixel spritehole 16 24), bottom $(pixel spritehole 16 28)"
# ---- Bigger than a tile ----
#
# Two by two, so four tiles in reading order from the one named: 4 and 5 across the top, 6 and
# 7 underneath. Each is its own colour, which is the only way to catch a sprite that draws all
# four in the right places in the wrong order.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x00
epilogue
} | run spritebig || exit 1
[ "$(pixel spritebig 16 24)" = "255,0,0" ] && [ "$(pixel spritebig 24 24)" = "0,255,0" ] \
&& [ "$(pixel spritebig 16 32)" = "0,0,255" ] && [ "$(pixel spritebig 24 32)" = "255,255,0" ] \
&& result ok "a sprite is m by n tiles" "four of them, in reading order" \
|| result no "a sprite is m by n tiles" "$(pixel spritebig 16 24) $(pixel spritebig 24 24) $(pixel spritebig 16 32) $(pixel spritebig 24 32)"
# Mirrored, which has to move the TILES and not only the pixels inside them - a two tile wide
# thing whose halves stayed put would turn inside out rather than round.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x01
epilogue
} | run spriteflip || exit 1
[ "$(pixel spriteflip 16 24)" = "0,255,0" ] && [ "$(pixel spriteflip 24 24)" = "255,0,0" ] \
&& result ok "mirroring moves the tiles too" "the right hand tile came out on the left" \
|| result no "mirroring moves the tiles too" "$(pixel spriteflip 16 24) $(pixel spriteflip 24 24)"
# And upside down, the same argument on the other axis.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x02
epilogue
} | run spriteover || exit 1
[ "$(pixel spriteover 16 24)" = "0,0,255" ] && [ "$(pixel spriteover 16 32)" = "255,0,0" ] \
&& result ok "and turning it over does as well" "the bottom tile came out on top" \
|| result no "and turning it over does as well" "$(pixel spriteover 16 24) $(pixel spriteover 16 32)"
# ---- Off the edge ----
#
# The reason the position is signed. Four pixels off the left is half a tile showing; a whole
# tile off is nothing at all, and must be nothing rather than a wrapped one at the far side.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 -4 24 0x11 0x00
spriteAt 1 1 0x00 -8 40 0x11 0x00
epilogue
} | run spriteedge || exit 1
[ "$(pixel spriteedge 0 24)" = "255,0,0" ] && [ "$(pixel spriteedge 4 24)" = "0,0,0" ] \
&& result ok "a sprite can sit off the edge" "half of it showing, and half not" \
|| result no "a sprite can sit off the edge" "at 0 $(pixel spriteedge 0 24), at 4 $(pixel spriteedge 4 24)"
[ "$(pixel spriteedge 0 40)" = "0,0,0" ] && [ "$(pixel spriteedge 312 40)" = "0,0,0" ] \
&& result ok "and right off it is gone" "not wrapped round to the other side" \
|| result no "and right off it is gone" "left $(pixel spriteedge 0 40), right $(pixel spriteedge 312 40)"
# ---- In front, and behind ----
#
# Behind means drawn only where the background had NOTHING - the same rule that makes a
# sprite's own nought a hole, read the other way round. The cell is solid on top and empty
# underneath, so a sprite behind it shows through the bottom half only.
{ prologue
spriteColours
halfTile 2 0x01 0x00
pokeScreen 0x4304 0x02; pokeScreen 0x4305 0x01 # Row 3, column 2: tile 2 in scheme 1.
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x11 0x04
epilogue
} | run spritebehind || exit 1
[ "$(pixel spritebehind 16 24)" = "0,255,255" ] && [ "$(pixel spritebehind 16 28)" = "255,0,0" ] \
&& result ok "a sprite can go behind the map" "hidden where the cell had something" \
|| result no "a sprite can go behind the map" "top $(pixel spritebehind 16 24), bottom $(pixel spritebehind 16 28)"
# Where two overlap, the lower number is in front. Both solid, both at the same place, and
# the one that wins says which way round the table is read.
{ prologue
spriteColours
solidTile 1 0x01; solidTile 2 0x02
spriteAt 0 1 0x00 16 24 0x11 0x00
spriteAt 1 2 0x00 16 24 0x11 0x00
epilogue
} | run spriteorder || exit 1
[ "$(pixel spriteorder 16 24)" = "255,0,0" ] \
&& result ok "the lower number is in front" "sprite nought covered sprite one" \
|| result no "the lower number is in front" "got $(pixel spriteorder 16 24)"
# ---- Nothing, which is what the table wakes up as ----
#
# A size of nought either way draws nothing, and that is the off switch. Everything above
# would pass on a device that drew every entry regardless, because every other entry in those
# tables happens to be zeroed - this is the one that says zero MEANS something.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x01 0x00
spriteAt 1 1 0x00 40 24 0x10 0x00
spriteAt 2 1 0x00 64 24 0x11 0x00
epilogue
} | run spritenone || exit 1
[ "$(pixel spritenone 16 24)" = "0,0,0" ] && [ "$(pixel spritenone 40 24)" = "0,0,0" ] \
&& [ "$(pixel spritenone 64 24)" = "255,0,0" ] \
&& result ok "no width or no height draws nothing" "and the one beside them still does" \
|| result no "no width or no height draws nothing" "$(pixel spritenone 16 24) $(pixel spritenone 40 24) $(pixel spritenone 64 24)"
# ---- Two screens, and the flip between them ----
#
# One red cell in each screen, in different rows: row one of the screen being shown, and the
@@ -1054,6 +1248,35 @@ timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flipafter.keys" \
&& result ok "and the system puts the screen back" "black again, and not the filled screen" \
|| result no "and the system puts the screen back" "commonest colour $(commonest "$BUILD/flipafter.ppm")"
# ---- A sprite, from inside the system ----
#
# Sprite moves a ball across the shell's own text and writes NOT ONE BYTE of the map to do
# it. The ball is 52 pixels of scheme one's ink, drawn from a tile whose corners are index
# nought - so counting that exact colour finds the ball and nothing else, the shell printing
# in grey.
python3 -c "open('$BUILD/ball.keys','wb').write(b'Sprite\n' + b'\x00'*40000)"
timeout 30 "$EMU" --fast --cycles 60000000 --keyboard "$BUILD/ball.keys" \
--screen "$BUILD/ball.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/ball.out" 2>&1 || true
[ "$(countColour "$BUILD/ball.ppm" d04038)" = "52" ] \
&& result ok "a program can put a sprite up" "52 pixels of ball, and a round one" \
|| result no "a program can put a sprite up" "$(countColour "$BUILD/ball.ppm" d04038) pixels, not 52"
# ---- And the system takes it down ----
#
# The sprite table is in the atlas at 0xC000, and the screen save walks the pages either side
# of it: to the end of the map, then the palette. So a sprite is not something the system can
# GIVE BACK, and Sprite deliberately does not clear its own - a program that faulted could
# not have either. What must not happen is a ball left sitting over the prompt, in front of
# everything, with nothing able to type it away.
python3 -c "open('$BUILD/ballgone.keys','wb').write(b'Sprite\n' + b'\x00'*600 + b' ' + b'\x00'*600)"
timeout 30 "$EMU" --fast --cycles 60000000 --keyboard "$BUILD/ballgone.keys" \
--screen "$BUILD/ballgone.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/ballgone.out" 2>&1 || true
[ "$(countColour "$BUILD/ballgone.ppm" d04038)" = "0" ] \
&& result ok "and the system takes the sprite down" "not one pixel of it left over the shell" \
|| result no "and the system takes the sprite down" "$(countColour "$BUILD/ballgone.ppm" d04038) pixels still there"
# ---- Clearing puts the cursor back at the top ----
#
# A screen with nothing on it and a cursor half way down it is not a cleared screen. This