diff --git a/Programs/Examples/colours.asm b/Programs/Examples/colours.asm index 1c39bfb..2c1032d 100644 --- a/Programs/Examples/colours.asm +++ b/Programs/Examples/colours.asm @@ -89,9 +89,9 @@ nextBank: INIA 0x03 OUTA 0xE8 ; Command: RegisterBank - ; The palette starts at 0xC000, and entry n is at n times four. Bank 2's ink is entry - ; 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xC084. - INIA 0xC0 + ; The palette sits at the top of video memory, at 0xFC00, and entry n is at n times + ; four. Bank 2's ink is entry 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xFC84. + INIA 0xFC OUTA 0xE4 ; DestHigh INIA 0x84 OUTA 0xE5 ; DestLow diff --git a/Programs/Examples/picture.asm b/Programs/Examples/picture.asm new file mode 100644 index 0000000..92a85fb --- /dev/null +++ b/Programs/Examples/picture.asm @@ -0,0 +1,105 @@ +; picture.asm +; The other kind of screen: a byte a pixel. +; Written by Anachronaut +; +; ---- Two ways to have a screen ---- +; +; A tile mode costs the machine the number of CELLS that changed. Forty by twenty-five is +; two thousand bytes for a whole screen and four bytes for two cells, which is why text on +; this machine is affordable at all. +; +; A bitmap costs it the number of PIXELS. Three hundred and twenty by two hundred is 64,000 +; bytes - four frames of work at a megahertz to replace all of it. So this is not the mode to +; animate a whole screen in; it is the mode to draw a picture in and then leave alone, or to +; change a corner of. +; +; It lives over the top of tile memory and the map, because there is nowhere else for it: the +; bank is 65,536 bytes and the picture is 64,000 of them. Going to bitmap mode does not clear +; the text screen, it stops calling it one - and coming back finds the tiles holding whatever +; the picture put there. Taking the screen means taking it. +; +; The palette is the one thing that means the same in both, which is why it sits at the very +; top, out of the way of everything. + +#Program + +start: + ; Video memory is the screen's, not this program's, so it is reached the way every device's + ; memory is: given a bank number, then written through the memory controller. Banks 0, 1 + ; and 2 belong to the machine, so 3 is the first one software may hand out. + INIA 0d3 + OUTA 0xE3 ; DestBank: the number being given + INIA 0x30 + OUTA 0xE2 ; SourceLow: the port that owns the memory + INIA 0x03 + OUTA 0xE8 ; Command: RegisterBank + + ; ---- Two hundred and fifty six colours ---- + ; + ; Entry n at 0xFC00 plus n times four. Writing the controller's Data port puts a byte at + ; the destination and steps it on, so the whole palette is one address and a loop. + INIA 0xFC + OUTA 0xE4 + RSTA + OUTA 0xE5 + SETD.0 Count + STA.0 ; Still the zero from DestLow above: SETD does not touch A +palette: + LDA.0 + OUTA 0xE9 ; red climbs + LDA.0 + OUTA 0xE9 ; green with it + LDA.0 + INIB 0xFF + XOR + MVQA + OUTA 0xE9 ; and blue falls away, so it runs blue to white to yellow + RSTA + OUTA 0xE9 ; the fourth byte is spare + LDA.0 + INCA + STA.0 + BNA palette ; A comes back to zero after 256 of them + + ; ---- The picture ---- + ; + ; Two hundred rows of three hundred and twenty pixels, each row one colour. FILL LEAVES THE + ; DESTINATION PAST WHAT IT TOUCHED, so the address is set once here and never worked out + ; again - which matters, because working out where row n begins would be n times 320 and + ; this machine has no multiply. + RSTA + OUTA 0xE4 + OUTA 0xE5 ; Dest 0x0000, the top left corner + INIA 0x01 + OUTA 0xE6 + INIA 0x40 + OUTA 0xE7 ; 320 bytes, which is one row + SETD.0 Count + RSTA + STA.0 +rows: + LDA.0 + OUTA 0xE2 ; Fill takes its byte from SourceLow: the row number is the colour + INIA 0x02 + OUTA 0xE8 ; Command: Fill + LDA.0 + INCA + STA.0 + INIB 0d200 + CCF + SUB + BNQ rows + + ; And now show it. Nothing above cared which mode the screen was in - the bytes were + ; already there, waiting to be called a picture. + INIA 0x02 + OUTA 0x31 + HALT + +#Data + +Count: + 0x00 + +#Vectors + Boot start diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 582b5b8..08b0cda 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -301,6 +301,19 @@ static void consoleSayCursor(void) { } static void consoleDraw(uint8_t byte) { + // ---- Nowhere to put a glyph ---- + // + // A console is a display controller, and a display controller draws characters on a + // character screen. In bitmap mode there is not one - the memory it would write into is + // somebody's picture - so it draws nothing and says everything down the serial line + // instead, which is where it was always going as well. + // + // The alternative is what a machine with shared video memory really does, which is + // scribble. That is honest and useless: nobody can read the marks and they ruin the + // picture, and a program that has taken the screen has not stopped wanting to print. + if (videoTextRows() == 0) { + return; + } switch (byte) { case '\n': consoleNewLine(); diff --git a/Source/Emulator/video.c b/Source/Emulator/video.c index 611c691..29f2b40 100644 --- a/Source/Emulator/video.c +++ b/Source/Emulator/video.c @@ -28,8 +28,18 @@ static uint8_t pixels[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT * 3]; static int renderedWidth = 0; static int renderedHeight = 0; -static int columnsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 80 : 40; } -static int rowsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 50 : 25; } +// Zero in bitmap mode, where there are no characters. Everything that draws one checks, so +// this is the single place the answer lives rather than a mode test in each of them. +static int columnsFor(uint8_t m) { + if (m == VIDEO_MODE_BITMAP) return 0; + return m == VIDEO_MODE_80x50 ? 80 : 40; +} +static int rowsFor(uint8_t m) { + if (m == VIDEO_MODE_BITMAP) return 0; + return m == VIDEO_MODE_80x50 ? 50 : 25; +} + +int videoTextRows(void) { return rowsFor(mode); } int videoColumns(void) { return columnsFor(mode); } int videoRows(void) { return rowsFor(mode); } @@ -231,6 +241,26 @@ uint8_t videoRead(uint8_t port) { } void videoRender(void) { + if (mode == VIDEO_MODE_BITMAP) { + // ---- A byte a pixel, and nothing in the way ---- + // + // No tile to look up and no attribute to add: the byte IS the palette index. Which + // is the whole difference between the two kinds of screen - a tile mode costs the + // CPU the number of cells that changed, and this costs it the number of pixels. + const uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE; + const uint8_t *from = videoRAM + VIDEO_BITMAP_BASE; + uint8_t *out = pixels; + for (int at = 0; at < VIDEO_BITMAP_WIDTH * VIDEO_BITMAP_HEIGHT; at++) { + const uint8_t *entry = palette + from[at] * VIDEO_PALETTE_BYTES; + *out++ = entry[0]; + *out++ = entry[1]; + *out++ = entry[2]; + } + renderedWidth = VIDEO_BITMAP_WIDTH; + renderedHeight = VIDEO_BITMAP_HEIGHT; + return; + } + const int columns = columnsFor(mode); const int rows = rowsFor(mode); const int width = columns * VIDEO_CELL_PIXELS; diff --git a/Source/Emulator/video.h b/Source/Emulator/video.h index c7a334b..973f220 100644 --- a/Source/Emulator/video.h +++ b/Source/Emulator/video.h @@ -51,12 +51,30 @@ // Two bytes to a cell: which tile, and how to colour it. #define VIDEO_CELL_BYTES 2 +// ---- A bitmap, over the top of the tiles and the map ---- +// +// THE SAME MEMORY MEANING DIFFERENT THINGS IN DIFFERENT MODES, which is what shared video +// memory has always been. There is no room for it to be anywhere else: 320 by 200 at a byte +// a pixel is 64,000 bytes and the whole bank is 65,536, so a bitmap that sat beside the +// tiles rather than on top of them would need a second bank for no reason except tidiness. +// +// What it costs is that the two do not coexist. Going to bitmap mode does not clear the text +// screen; it stops calling it a text screen. Coming back finds the tiles and the map holding +// whatever the picture put there, which is what taking the screen means. +#define VIDEO_BITMAP_BASE 0x0000 +#define VIDEO_BITMAP_WIDTH 320 +#define VIDEO_BITMAP_HEIGHT 200 + // ---- The palette ---- // // Four bytes an entry rather than three, for the same reason a map row is a page: entry n // begins at n times four, which is a shift. Three would need a multiply the machine does // not have. The fourth byte is unused and reads as whatever was put there. -#define VIDEO_PALETTE_BASE 0xC000 +// +// At the TOP of video memory, clear of everything else, because it is the one thing that has +// to mean the same in every mode - a bitmap needs colours as much as a tile does, and 64,000 +// bytes of picture leaves nowhere in the middle for it to hide. +#define VIDEO_PALETTE_BASE 0xFC00 #define VIDEO_PALETTE_BYTES 4 #define VIDEO_PALETTE_SIZE 256 @@ -65,14 +83,20 @@ // Both are 8x8 cells over the same engine; only how many of them differ. The pixel count // costs the CPU nothing, because it only ever writes the map - which is why the larger mode // is affordable at all. -#define VIDEO_MODE_40x25 0 -#define VIDEO_MODE_80x50 1 -#define VIDEO_MODE_COUNT 2 +#define VIDEO_MODE_40x25 0 +#define VIDEO_MODE_80x50 1 +#define VIDEO_MODE_BITMAP 2 +#define VIDEO_MODE_COUNT 3 #define VIDEO_CELL_PIXELS 8 #define VIDEO_MAX_WIDTH (80 * VIDEO_CELL_PIXELS) #define VIDEO_MAX_HEIGHT (50 * VIDEO_CELL_PIXELS) +// How many characters across and down the screen is, and ZERO IN BITMAP MODE, where there is +// no such thing. The console asks, and a console told there are no columns has nowhere to +// put a glyph and does not try. +int videoTextRows(void); + // ---- Ports ---- // // Sixteen, like the controller, and it interrupts on its base the way the disk established. diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index db8a43e..b949d9b 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -532,7 +532,12 @@ One bank, brought by the device and reached only through the memory controller, | --- | --- | | 0x0000 - 0x3FFF | Tile memory. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. | | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. | -| 0xC000 - 0xC3FF | The palette. 256 entries of four bytes: red, green, blue, and one unused. | +| 0x0000 - 0xF9FF | In bitmap mode, the picture instead: 64,000 bytes, one to a pixel. | +| 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused. | + +**The bitmap is the same memory as the tiles and the map**, which is what shared video memory has always been, and there is nowhere else it could be: 64,000 bytes of picture in a 65,536 byte bank leaves room for nothing beside it. Going to bitmap mode does not clear the text screen - it stops calling it one, and coming back finds the tiles and the map holding whatever the picture put there. + +The palette is at the top, out of the way of both, because it is the one thing that means the same in every mode. **A map row is a page whether the mode fills it or not**, and that is arithmetic rather than waste. This machine has no multiply, so on a 40 column screen every cursor move would otherwise cost a `row times 40` in software - a tax on the most common operation in the system. At a page a row there is no arithmetic at all: the row number is the high byte of the address and the doubled column is the low byte. @@ -561,8 +566,13 @@ The high nibble is reserved and should be left at zero, so that a meaning can be | --- | --- | --- | | 0 | 320 by 200 | 40 by 25 | | 1 | 640 by 400 | 80 by 50 | +| 2 | 320 by 200 | none: a byte a pixel | -Both are 8 by 8 cells over the same engine, and the pixel count costs a program nothing, because it only ever writes the map. A mode that does not exist is not taken, and is not a fault either: a screen is a poor place to stop the machine, and a program that asked for something impossible still has the screen it had. +The first two are 8 by 8 cells over the same engine, and the pixel count costs a program nothing, because it only ever writes the map. + +**Mode 2 is the other kind of screen**, where a byte is a palette index and there is no tile to look it up in and no attribute to add. What it costs is the other way round: a whole picture is 64,000 bytes, four frames of work at a megahertz, so it is the mode to draw in and leave alone or to change a corner of, not the mode to animate all of. `Programs/Examples/picture.asm` fills one in 127 bytes of program. + +**A bitmap has no columns and no rows**, and asking says so: both registers read zero, which is the true answer rather than a leftover from the last mode. The console asks, and a console told there is no character screen has nowhere to put a glyph and draws nothing - it still says everything down the serial line. The alternative is what a machine with shared video memory really does, which is scribble marks nobody can read across somebody's picture. A mode that does not exist is not taken, and is not a fault either: a screen is a poor place to stop the machine, and a program that asked for something impossible still has the screen it had. How big the screen is, is asked for rather than assumed. A program written once can find out what it is running on. @@ -634,7 +644,7 @@ So `attribute XOR 8` turns any pair inside out, which is what a highlighted line **That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up. -The palette lives at 0xC000 in video memory, four bytes an entry - red, green, blue, and one spare - so entry *n* begins at 0xC000 plus *n* times four. Video memory belongs to the screen rather than to the program, so it is written the way every device's memory is written: registered as a bank, and reached through the memory controller. +The palette lives at 0xFC00 in video memory, four bytes an entry - red, green, blue, and one spare - so entry *n* begins at 0xFC00 plus *n* times four. Video memory belongs to the screen rather than to the program, so it is written the way every device's memory is written: registered as a bank, and reached through the memory controller. `Programs/Examples/colours.asm` does all of that in eighty lines and prints the result. It shows the sixteen pairs, shows what XOR 8 does to each, and then changes one of them by writing three bytes into the palette, so that the difference between using the colours a machine wakes up with and choosing your own is visible in one program. diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index b4d7b09..9196615 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -78,7 +78,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`. 168 tests, of which 106 run, 35 +everything it printed against a file in `Tests/expected`. 169 tests, of which 107 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/picture.out b/Tests/expected/picture.out new file mode 100644 index 0000000..6062395 --- /dev/null +++ b/Tests/expected/picture.out @@ -0,0 +1,2 @@ +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 5e72e37..25fc346 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -49,6 +49,9 @@ colours | Examples/colours.asm | run | - # here because the count is exact: a frame is counted in the machine's own cycles, so the # same program sees the same sixty however fast the host really went. frames | Examples/frames.asm | run | - | - +# picture draws a whole 320 by 200 bitmap and prints nothing, so what is recorded is only +# that it ran and what it cost. Tests/video.sh is where the pixels are checked. +picture | Examples/picture.asm | run | - | - 8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | - 16bitSegmentedSieve | Examples/primeSieve/16bitSieve.asm | run | - | - # The four pointer rewrite. It emits exactly the same primes as the line above, which diff --git a/Tests/video.sh b/Tests/video.sh index 43928a6..e2ebe7d 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -64,7 +64,7 @@ ASM # run, so palette entry 0 is the console's paper rather than black. A check that wanted # black and got paper would be a check that had quietly depended on a default. These # tests are about the device, so they set what they are about to look at. - poke 0xC000 0x00; poke 0xC001 0x00; poke 0xC002 0x00 + poke 0xFC00 0x00; poke 0xFC01 0x00; poke 0xFC02 0x00 } poke() { @@ -204,7 +204,7 @@ echo "Checking what the video device draws." # and column 3 of row 2. A tile drawn one cell out is the commonest way a tile engine is # wrong, so the check is where it is AND where it is not. { prologue - poke 0xC004 0xFF; poke 0xC005 0x00; poke 0xC006 0x00 + poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00 for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01; poke 0x4001 0x00 poke $((0x4000 + 2 * 256 + 3 * 2)) 0x01 @@ -229,7 +229,7 @@ echo "Checking what the video device draws." # Same tile, same map, a different palette entry. Nothing about the picture changes except # the three bytes the colour came from. { prologue - poke 0xC004 0x00; poke 0xC005 0xFF; poke 0xC006 0x40 + poke 0xFC04 0x00; poke 0xFC05 0xFF; poke 0xFC06 0x40 for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01; poke 0x4001 0x00 epilogue @@ -245,8 +245,8 @@ echo "Checking what the video device draws." # the only difference between the two cells is the attribute nibble: 0 leaves the index # alone, 1 adds sixteen. This is the whole of the recolouring feature in one check. { prologue - poke 0xC004 0xFF; poke 0xC005 0x00; poke 0xC006 0x00 - poke 0xC044 0x00; poke 0xC045 0x00; poke 0xC046 0xFF + poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00 + poke 0xFC44 0x00; poke 0xFC45 0x00; poke 0xFC46 0xFF for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01; poke 0x4001 0x00 poke 0x4002 0x01; poke 0x4003 0x01 @@ -266,7 +266,7 @@ echo "Checking what the video device draws." # row to the top of the screen, which is the whole reason a terminal on this machine is # affordable at all. { prologue - poke 0xC004 0xFF; poke 0xC005 0xFF; poke 0xC006 0x00 + poke 0xFC04 0xFF; poke 0xFC05 0xFF; poke 0xFC06 0x00 for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke $((0x4000 + 3 * 256)) 0x01 port 0x34 0x03 @@ -285,7 +285,7 @@ echo "Checking what the video device draws." # Origin 127 with 128 rows puts map row 127 at the top and map row 0 immediately under it. # A map that clipped instead of wrapping would show nothing on the second row. { prologue - poke 0xC004 0xFF; poke 0xC005 0xFF; poke 0xC006 0xFF + poke 0xFC04 0xFF; poke 0xFC05 0xFF; poke 0xFC06 0xFF for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01 port 0x34 0x7F @@ -535,6 +535,57 @@ coloured blinkagain 0 0 "216,216,216" \ && result ok "and lit again after that" "which is what blinking is" \ || result no "and lit again after that" "got $(pixel blinkagain 0 0)" +# ---- A byte a pixel ---- +# +# The other kind of screen. No tile to look up and no attribute to add: the byte IS the +# palette index, and it lives over the top of the tiles and the map, because 64,000 bytes of +# picture leaves room for nothing else in a 65,536 byte bank. + +# Palette entry 5, then one pixel of it at row 2, column 3 - which is byte 2*320+3 = 643. +{ prologue + poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90 + poke 0x0283 0x05 + port 0x31 0x02 + epilogue +} | run bitmap || exit 1 +[ "$(size bitmap)" = "320 200" ] \ + && result ok "bitmap mode is 320 by 200" "$(size bitmap)" \ + || result no "bitmap mode is 320 by 200" "got $(size bitmap)" +coloured bitmap 3 2 "32,192,144" \ + && result ok "and a byte is a pixel's colour" "byte 643 is row 2, column 3" \ + || result no "and a byte is a pixel's colour" "got $(pixel bitmap 3 2)" +coloured bitmap 4 2 "0,0,0" \ + && result ok "and only that pixel" "the one beside it is untouched" \ + || result no "and only that pixel" "got $(pixel bitmap 4 2)" + +# ---- And the console keeps off it ---- +# +# There is no character screen in bitmap mode, so there is nowhere to put a glyph. The +# alternative is what a machine with shared video memory really does, which is scribble on +# somebody's picture with marks nobody can read. It still says everything down the serial +# line, which is where it was going as well. +{ prologue + poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90 + poke 0x0283 0x05 + port 0x31 0x02 + say "A" + epilogue +} | run bitmaptext || exit 1 +coloured bitmaptext 3 2 "32,192,144" \ + && result ok "printing does not touch a bitmap" "the pixel survived a letter" \ + || result no "printing does not touch a bitmap" "got $(pixel bitmaptext 3 2)" +[ "$(said bitmaptext)" = "65" ] \ + && result ok "and the letter still goes out" "down the serial line" \ + || result no "and the letter still goes out" "got $(said bitmaptext)" + +# Asking how many columns there are in bitmap mode is asking about something that is not +# there, and nought is the true answer rather than a leftover from the last mode. +{ prologue; port 0x31 0x02; show 0x32; port 0x31 0x00; show 0x32; epilogue +} | run bitmapsize || exit 1 +[ "$(said bitmapsize)" = "0 40" ] \ + && result ok "a bitmap has no columns" "and forty again when it is text" \ + || result no "a bitmap has no columns" "got $(said bitmapsize)" + # ---- The frame, which is the only beat this machine has ---- # # There is no clock. Every program that wanted to happen at a certain speed has until now