diff --git a/Programs/CosmOS/Apps/Sprite.asm b/Programs/CosmOS/Apps/Sprite.asm new file mode 100644 index 0000000..3d87dd2 --- /dev/null +++ b/Programs/CosmOS/Apps/Sprite.asm @@ -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 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 4288fad..3214115 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -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 diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 520165b..5fe2a39 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -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 diff --git a/Source/Emulator/video.c b/Source/Emulator/video.c index e7276cf..adb8704 100644 --- a/Source/Emulator/video.c +++ b/Source/Emulator/video.c @@ -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; } diff --git a/Source/Emulator/video.h b/Source/Emulator/video.h index 63533eb..a741018 100644 --- a/Source/Emulator/video.h +++ b/Source/Emulator/video.h @@ -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 diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 6afb0bf..ba4488f 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -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: diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index 552ce1e..92c7ebb 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -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. diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index fb61fe9..84740fe 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -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. diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 3227acc..72df682 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -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 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index 64868e0..6e04969 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -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. diff --git a/Tests/expected/cosmosFlip.out b/Tests/expected/cosmosFlip.out index 1162360..54414d1 100644 --- a/Tests/expected/cosmosFlip.out +++ b/Tests/expected/cosmosFlip.out @@ -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. diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 014eb78..98cb9a5 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -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. diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index 7e82e35..d5edf6e 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -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. diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index a04e0b9..4f1b3b8 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -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. diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 0adb628..0f2d3fb 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -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 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index c459036..43fc77c 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -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 diff --git a/Tests/expected/cosmosSprite.out b/Tests/expected/cosmosSprite.out new file mode 100644 index 0000000..2cbefb1 --- /dev/null +++ b/Tests/expected/cosmosSprite.out @@ -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