The screen is two banks: an atlas and a screen
Tiles and colours are written when a program loads; the map is written whenever anything moves. Sharing one 64K bank made them compete for room neither needed all of, and had a worse consequence than being cramped: a bitmap covers the whole bank, so entering bitmap mode destroyed the font. A program could not draw a picture and then say anything about it. Split, each gets a whole bank. The atlas holds the tiles and the palette, the screen holds the map or a bitmap, and a picture now costs the map and nothing else. It also leaves 48K free in the atlas, which is where the sprite table and a second page of tiles are going. No new mechanism was needed. A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory: the base port keeps the atlas, since tiles have been at 0x0000 since there was a screen at all, and 0x3A owns the screen. The registry now answers honestly about which ports in the block bring memory, where it used to say all sixteen did. CosmOS never addresses video memory except in one place - the screen save, which walks 196 pages of it. The page number already says which bank a page is in, so screenBankFor works it out rather than keeping a second list beside screenPageFor. Grid and picture.asm register both banks; colours.asm only touches the palette and needed none of it. Tests/video.sh names the memory every write is for, because an address cannot: tile 5 and bitmap pixel 5 are both 0x0005, and a helper that guessed would be right for the tiles and silently wrong for a picture. And picture.asm gained a check, because this change broke it and nothing noticed - registering the second bank leaves DestBank pointing at it, so the palette went into the wrong one and the picture came out black. It was the only thing here found by looking rather than by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
2abc8281df
commit
66e7b84272
@@ -37,12 +37,23 @@ start:
|
|||||||
; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the
|
; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the
|
||||||
; filesystem made after that came out of video memory, so the shell found an empty disk
|
; filesystem made after that came out of video memory, so the shell found an empty disk
|
||||||
; and could not start anything by name. Nothing said a word.
|
; and could not start anything by name. Nothing said a word.
|
||||||
|
; TWO BANKS, because the screen has two. The atlas holds the tiles and the palette and is
|
||||||
|
; written when a program starts; the screen holds the map and is written as things move.
|
||||||
|
; They are separate memories, so a bank number reaches one or the other and never both -
|
||||||
|
; which is the whole of what this program had to learn when they were split apart.
|
||||||
INIA 0d4
|
INIA 0d4
|
||||||
OUTA 0xE3 ; DestBank: the number it will answer to.
|
OUTA 0xE3 ; DestBank: the number it will answer to.
|
||||||
INIA 0x30
|
INIA 0x30
|
||||||
OUTA 0xE2 ; SourceLow: the port of the device that owns it.
|
OUTA 0xE2 ; SourceLow: the port of the device that owns it.
|
||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8 ; RegisterBank.
|
OUTA 0xE8 ; RegisterBank. Four is the atlas.
|
||||||
|
|
||||||
|
INIA 0d5
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x3A
|
||||||
|
OUTA 0xE2 ; And the port that owns the screen.
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; RegisterBank. Five is the map.
|
||||||
|
|
||||||
; ---- Asking for the screen back afterwards ----
|
; ---- Asking for the screen back afterwards ----
|
||||||
;
|
;
|
||||||
@@ -153,7 +164,7 @@ finished:
|
|||||||
OUTA 0x34
|
OUTA 0x34
|
||||||
OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
|
OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
|
||||||
|
|
||||||
INIA 0d4
|
INIA 0d5
|
||||||
OUTA 0xE3
|
OUTA 0xE3
|
||||||
INIA 0x40
|
INIA 0x40
|
||||||
OUTA 0xE4
|
OUTA 0xE4
|
||||||
@@ -350,7 +361,7 @@ putMap:
|
|||||||
STA.0
|
STA.0
|
||||||
|
|
||||||
everyRow:
|
everyRow:
|
||||||
INIA 0d4
|
INIA 0d5
|
||||||
OUTA 0xE3
|
OUTA 0xE3
|
||||||
SETD.0 MapRow
|
SETD.0 MapRow
|
||||||
LDA.0
|
LDA.0
|
||||||
|
|||||||
@@ -5147,7 +5147,13 @@ screenBank:
|
|||||||
INIA 0x30
|
INIA 0x30
|
||||||
OUTA 0xE2
|
OUTA 0xE2
|
||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8
|
OUTA 0xE8 ; Four is the atlas: the tiles and the palette.
|
||||||
|
INIA 0d5
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x3A
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; Five is the screen: the map, or a bitmap.
|
||||||
RET
|
RET
|
||||||
|
|
||||||
; Back to the drive whoever called was standing on.
|
; Back to the drive whoever called was standing on.
|
||||||
@@ -5178,10 +5184,42 @@ screenPageDirect:
|
|||||||
ADD
|
ADD
|
||||||
RET
|
RET
|
||||||
|
|
||||||
|
; ---- And which of the screen's two banks that page is in ----
|
||||||
|
;
|
||||||
|
; The tiles and the palette are the atlas and the map is the screen, so the PAGE NUMBER
|
||||||
|
; ALREADY SAYS WHICH - 0x00 to 0x3F and 0xFC to 0xFF one way, 0x40 to 0xBF the other. Nothing
|
||||||
|
; has to be remembered alongside the page, and there is no second list to keep in step with
|
||||||
|
; screenPageFor above.
|
||||||
|
;
|
||||||
|
; IN Q AND NOT A, for the same reason screenPageFor answers there: RET puts A back the way it
|
||||||
|
; found it, so a subroutine that answered in A would answer with the argument it was given.
|
||||||
|
screenBankFor:
|
||||||
|
INIB 0x40
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC screenBankAtlas ; Borrowed, so it is below 0x40 and is a tile.
|
||||||
|
INIB 0xC0
|
||||||
|
CCF
|
||||||
|
SUB
|
||||||
|
BRC screenBankScreen ; Borrowed, so it is below 0xC0 and is the map.
|
||||||
|
screenBankAtlas:
|
||||||
|
INIA 0d4
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
screenBankScreen:
|
||||||
|
INIA 0d5
|
||||||
|
RSTB
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
RET
|
||||||
|
|
||||||
; A holds a page of video memory. Its 256 bytes come into ScreenBlock.
|
; A holds a page of video memory. Its 256 bytes come into ScreenBlock.
|
||||||
screenFromVideo:
|
screenFromVideo:
|
||||||
PSHA
|
CALL screenBankFor ; A is the page and RET hands it back; Q comes back the bank.
|
||||||
INIA 0d4
|
PSHA ; The page, which the bank is about to want A for.
|
||||||
|
MVQA
|
||||||
OUTA 0xE0
|
OUTA 0xE0
|
||||||
POPA
|
POPA
|
||||||
OUTA 0xE1
|
OUTA 0xE1
|
||||||
@@ -5197,8 +5235,9 @@ screenFromVideo:
|
|||||||
|
|
||||||
; A holds a page of video memory. ScreenBlock goes back into it.
|
; A holds a page of video memory. ScreenBlock goes back into it.
|
||||||
screenToVideo:
|
screenToVideo:
|
||||||
|
CALL screenBankFor
|
||||||
PSHA
|
PSHA
|
||||||
INIA 0d4
|
MVQA
|
||||||
OUTA 0xE3
|
OUTA 0xE3
|
||||||
POPA
|
POPA
|
||||||
OUTA 0xE4
|
OUTA 0xE4
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ nextBank:
|
|||||||
; three bytes into the palette - which means reaching video memory, which means the
|
; three bytes into the palette - which means reaching video memory, which means the
|
||||||
; controller.
|
; controller.
|
||||||
|
|
||||||
; Give the screen's memory a bank number. The screen answers on port 0x30, and bank 3 is
|
; Give the screen's memory a bank number. The screen brings TWO banks and this only wants
|
||||||
; the first number software is allowed to hand out: 0, 1 and 2 belong to the machine.
|
; one of them: port 0x30 owns the atlas, where the tiles and the palette are, and the
|
||||||
|
; palette is all this touches. Bank 3 is the first number software is allowed to hand out:
|
||||||
|
; 0, 1 and 2 belong to the machine.
|
||||||
INIA 0d3
|
INIA 0d3
|
||||||
OUTA 0xE3 ; DestBank: the number being given
|
OUTA 0xE3 ; DestBank: the number being given
|
||||||
INIA 0x30
|
INIA 0x30
|
||||||
@@ -89,7 +91,7 @@ nextBank:
|
|||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8 ; Command: RegisterBank
|
OUTA 0xE8 ; Command: RegisterBank
|
||||||
|
|
||||||
; The palette sits at the top of video memory, at 0xFC00, and entry n is at n times
|
; The palette sits at the top of the atlas, 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.
|
; four. Bank 2's ink is entry 2 * 16 + 1, which is 33, and 33 * 4 is 132 - so 0xFC84.
|
||||||
INIA 0xFC
|
INIA 0xFC
|
||||||
OUTA 0xE4 ; DestHigh
|
OUTA 0xE4 ; DestHigh
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
; animate a whole screen in; it is the mode to draw a picture in and then leave alone, or 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.
|
; change a corner of.
|
||||||
;
|
;
|
||||||
; It lives over the top of tile memory and the map, because there is nowhere else for it: the
|
; It lives over the top of the map, in the SCREEN bank, because that is the bank whose
|
||||||
; bank is 65,536 bytes and the picture is 64,000 of them. Going to bitmap mode does not clear
|
; contents change: a picture and a map are the same memory called two different things in two
|
||||||
; the text screen, it stops calling it one - and coming back finds the tiles holding whatever
|
; different modes. Going to bitmap mode does not clear the text screen, it stops calling it
|
||||||
; the picture put there. Taking the screen means taking it.
|
; one - and coming back finds the map holding whatever the picture put there.
|
||||||
;
|
;
|
||||||
; The palette is the one thing that means the same in both, which is why it sits at the very
|
; WHAT IT DOES NOT COST IS THE FONT. The tiles and the palette are in the other bank and a
|
||||||
; top, out of the way of everything.
|
; picture cannot reach them, which is what lets this program draw and then say something.
|
||||||
|
|
||||||
#Program
|
#Program
|
||||||
|
|
||||||
@@ -27,17 +27,31 @@ start:
|
|||||||
; Video memory is the screen's, not this program's, so it is reached the way every device's
|
; 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
|
; 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.
|
; and 2 belong to the machine, so 3 is the first one software may hand out.
|
||||||
|
; TWO OF THEM, because the screen brings two banks and this program writes to both: the
|
||||||
|
; palette is in the atlas and the picture is in the screen.
|
||||||
INIA 0d3
|
INIA 0d3
|
||||||
OUTA 0xE3 ; DestBank: the number being given
|
OUTA 0xE3 ; DestBank: the number being given
|
||||||
INIA 0x30
|
INIA 0x30
|
||||||
OUTA 0xE2 ; SourceLow: the port that owns the memory
|
OUTA 0xE2 ; SourceLow: the port that owns the memory
|
||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8 ; Command: RegisterBank
|
OUTA 0xE8 ; Command: RegisterBank. Three is the atlas.
|
||||||
|
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x3A
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; And four is the screen.
|
||||||
|
|
||||||
; ---- Two hundred and fifty six colours ----
|
; ---- Two hundred and fifty six colours ----
|
||||||
;
|
;
|
||||||
; Entry n at 0xFC00 plus n times four. Writing the controller's Data port puts a byte at
|
; 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.
|
; the destination and steps it on, so the whole palette is one address and a loop.
|
||||||
|
; A already holds 3, because RegisterBank is command 3 and the bank wanted is bank 3.
|
||||||
|
; Leaving the line out would turn a program that says which bank it means into one that
|
||||||
|
; works by coincidence, and the coincidence breaks the day either number changes.
|
||||||
|
INIA 0d3 ; splitlint[redundant-assignment]: said rather than coincided
|
||||||
|
OUTA 0xE3 ; The ATLAS, which registering the second bank moved off
|
||||||
INIA 0xFC
|
INIA 0xFC
|
||||||
OUTA 0xE4
|
OUTA 0xE4
|
||||||
RSTA
|
RSTA
|
||||||
@@ -67,6 +81,8 @@ palette:
|
|||||||
; DESTINATION PAST WHAT IT TOUCHED, so the address is set once here and never worked out
|
; 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
|
; again - which matters, because working out where row n begins would be n times 320 and
|
||||||
; this machine has no multiply.
|
; this machine has no multiply.
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3 ; Into the SCREEN bank now, which is where a picture is
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0xE4
|
OUTA 0xE4
|
||||||
OUTA 0xE5 ; Dest 0x0000, the top left corner
|
OUTA 0xE5 ; Dest 0x0000, the top left corner
|
||||||
|
|||||||
+18
-7
@@ -1304,11 +1304,12 @@ uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
|||||||
*capacity = DISK_BLOCK_BYTES;
|
*capacity = DISK_BLOCK_BYTES;
|
||||||
return diskBuffer;
|
return diskBuffer;
|
||||||
}
|
}
|
||||||
if (port == PORT_VIDEO) {
|
if (port == PORT_VIDEO || port == VIDEO_SCREEN) {
|
||||||
// Tiles, the map and the palette, in one bank. A program blits the part that
|
// Two banks: the atlas of tiles and colours on the base port, and the map or the
|
||||||
// changed and the rest stays as it was, which is the whole reason the screen is a
|
// bitmap on its own. A program blits the part that changed and the rest stays as it
|
||||||
// bank rather than a window onto a port.
|
// was, which is the whole reason the screen is memory rather than a window onto a
|
||||||
return videoMemory(capacity);
|
// port - and having two means a picture costs the map and not the font.
|
||||||
|
return videoMemory(port, capacity);
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -1369,8 +1370,18 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
|||||||
return deviceOnPort(PORT_DISK);
|
return deviceOnPort(PORT_DISK);
|
||||||
}
|
}
|
||||||
if (port > PORT_VIDEO && port <= PORT_VIDEO_TOP) {
|
if (port > PORT_VIDEO && port <= PORT_VIDEO_TOP) {
|
||||||
// Sixteen ports, one device, and the same rule again.
|
// Sixteen ports, one device, and the same rule again - with one difference, because
|
||||||
return deviceOnPort(PORT_VIDEO);
|
// this device owns TWO banks. Forwarding the whole block to the base record used to
|
||||||
|
// say that all sixteen ports brought memory, which was harmless only while nobody
|
||||||
|
// believed it: a program that enumerated the block and registered everything
|
||||||
|
// claiming memory would have faulted on the fourteen that have none.
|
||||||
|
//
|
||||||
|
// So the block answers honestly. The screen port says it brings memory because it
|
||||||
|
// does, and the rest of the block says it does not.
|
||||||
|
static const DeviceRecord videoScreenRecord =
|
||||||
|
{ VIDEO_SCREEN, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||||
|
static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 };
|
||||||
|
return (port == VIDEO_SCREEN) ? &videoScreenRecord : &videoPlainRecord;
|
||||||
}
|
}
|
||||||
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
||||||
return deviceOnPort(PORT_SOUND);
|
return deviceOnPort(PORT_SOUND);
|
||||||
|
|||||||
+26
-13
@@ -11,7 +11,11 @@
|
|||||||
// The bank the device brings. Registered by whoever enumerates the hardware, reached only
|
// The bank the device brings. Registered by whoever enumerates the hardware, reached only
|
||||||
// through the memory controller, and never by the CPU directly - the same arrangement the
|
// through the memory controller, and never by the CPU directly - the same arrangement the
|
||||||
// disk's buffer has always had.
|
// disk's buffer has always had.
|
||||||
static uint8_t videoRAM[VIDEO_MEMORY_BYTES];
|
// The two banks. Which one an address is in is a property of the address and never of the
|
||||||
|
// mode: tiles and the palette are always in the atlas, the map and a bitmap always in the
|
||||||
|
// screen. That is what makes the split cost nothing to think about at a call site.
|
||||||
|
static uint8_t videoAtlas[VIDEO_MEMORY_BYTES];
|
||||||
|
static uint8_t videoScreen[VIDEO_MEMORY_BYTES];
|
||||||
|
|
||||||
static uint8_t mode;
|
static uint8_t mode;
|
||||||
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
||||||
@@ -129,7 +133,7 @@ void videoLoadFont(void) {
|
|||||||
// can ask for it. A program that defined a tile of its own above the font and then wanted
|
// can ask for it. A program that defined a tile of its own above the font and then wanted
|
||||||
// its text back would have lost the tile to get it.
|
// its text back would have lost the tile to get it.
|
||||||
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
||||||
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
uint8_t *tile = videoAtlas + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||||
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
|
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
|
||||||
const unsigned char row = consoleFont[glyph * CONSOLE_FONT_BYTES + y];
|
const unsigned char row = consoleFont[glyph * CONSOLE_FONT_BYTES + y];
|
||||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||||
@@ -140,7 +144,7 @@ void videoLoadFont(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void videoLoadPalette(void) {
|
void videoLoadPalette(void) {
|
||||||
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||||
for (int bank = 0; bank < 8; bank++) {
|
for (int bank = 0; bank < 8; bank++) {
|
||||||
// Colour on black, and then the same colour as paper with black ink, sixteen banks
|
// Colour on black, and then the same colour as paper with black ink, sixteen banks
|
||||||
// apart so that one bit turns either into the other.
|
// apart so that one bit turns either into the other.
|
||||||
@@ -165,7 +169,7 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
|||||||
// less than a cell, and there is no such thing as less than a cell to write into.
|
// less than a cell, and there is no such thing as less than a cell to write into.
|
||||||
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
||||||
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
uint8_t *cell = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||||
+ mapColumn * VIDEO_CELL_BYTES;
|
+ mapColumn * VIDEO_CELL_BYTES;
|
||||||
cell[0] = tile;
|
cell[0] = tile;
|
||||||
cell[1] = attribute;
|
cell[1] = attribute;
|
||||||
@@ -178,11 +182,12 @@ void videoScrollUp(void) {
|
|||||||
// hundred rows of what has already been said, still sitting in the map.
|
// hundred rows of what has already been said, still sitting in the map.
|
||||||
const int bottom = rowsFor(mode) - 1;
|
const int bottom = rowsFor(mode) - 1;
|
||||||
const int mapRow = (scroll + bottom) % VIDEO_MAP_ROWS;
|
const int mapRow = (scroll + bottom) % VIDEO_MAP_ROWS;
|
||||||
memset(videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
memset(videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void videoReset(void) {
|
void videoReset(void) {
|
||||||
memset(videoRAM, 0, sizeof(videoRAM));
|
memset(videoAtlas, 0, sizeof(videoAtlas));
|
||||||
|
memset(videoScreen, 0, sizeof(videoScreen));
|
||||||
mode = VIDEO_MODE_40x25;
|
mode = VIDEO_MODE_40x25;
|
||||||
scroll = 0;
|
scroll = 0;
|
||||||
scrollColumn = 0;
|
scrollColumn = 0;
|
||||||
@@ -201,9 +206,17 @@ void videoReset(void) {
|
|||||||
videoLoadPalette();
|
videoLoadPalette();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *videoMemory(uint32_t *capacity) {
|
uint8_t *videoMemory(uint8_t port, uint32_t *capacity) {
|
||||||
*capacity = VIDEO_MEMORY_BYTES;
|
*capacity = VIDEO_MEMORY_BYTES;
|
||||||
return videoRAM;
|
if (port == VIDEO_STATUS) {
|
||||||
|
return videoAtlas;
|
||||||
|
}
|
||||||
|
if (port == VIDEO_SCREEN) {
|
||||||
|
return videoScreen;
|
||||||
|
}
|
||||||
|
// Every other port in the block owns no memory. Saying so is what stops a bank being
|
||||||
|
// registered onto one of them and pointing at nothing.
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t videoWrite(uint8_t value, uint8_t port) {
|
uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||||
@@ -320,8 +333,8 @@ void videoRender(void) {
|
|||||||
// No tile to look up and no attribute to add: the byte IS the palette index. Which
|
// 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
|
// 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.
|
// 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 *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||||
const uint8_t *from = videoRAM + VIDEO_BITMAP_BASE;
|
const uint8_t *from = videoScreen + VIDEO_BITMAP_BASE;
|
||||||
uint8_t *out = pixels;
|
uint8_t *out = pixels;
|
||||||
for (int at = 0; at < VIDEO_BITMAP_WIDTH * VIDEO_BITMAP_HEIGHT; at++) {
|
for (int at = 0; at < VIDEO_BITMAP_WIDTH * VIDEO_BITMAP_HEIGHT; at++) {
|
||||||
const uint8_t *entry = palette + from[at] * VIDEO_PALETTE_BYTES;
|
const uint8_t *entry = palette + from[at] * VIDEO_PALETTE_BYTES;
|
||||||
@@ -351,7 +364,7 @@ void videoRender(void) {
|
|||||||
// The ring. Rows that scrolled off the top are still in the map, which is what
|
// The ring. Rows that scrolled off the top are still in the map, which is what
|
||||||
// makes scrollback free rather than something the console has to keep itself.
|
// makes scrollback free rather than something the console has to keep itself.
|
||||||
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
||||||
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
const uint8_t *cells = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||||
for (int column = 0; column <= columns; column++) {
|
for (int column = 0; column <= columns; column++) {
|
||||||
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
const uint8_t tile = cells[mapColumn * VIDEO_CELL_BYTES];
|
const uint8_t tile = cells[mapColumn * VIDEO_CELL_BYTES];
|
||||||
@@ -377,7 +390,7 @@ void videoRender(void) {
|
|||||||
// tile that wants all 256 colours simply leaves the nibble at zero and gets
|
// tile that wants all 256 colours simply leaves the nibble at zero and gets
|
||||||
// them. One adder in hardware, and neither use costs the other anything.
|
// them. One adder in hardware, and neither use costs the other anything.
|
||||||
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
||||||
const uint8_t *art = videoRAM + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
const uint8_t *art = videoAtlas + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||||
// Where this row of the cell lands once the view has been slid up by the
|
// Where this row of the cell lands once the view has been slid up by the
|
||||||
// fine offset. Negative means it is the part of the top cell that is off
|
// fine offset. Negative means it is the part of the top cell that is off
|
||||||
@@ -395,7 +408,7 @@ void videoRender(void) {
|
|||||||
// high end of the palette with a nibble set comes round the bottom,
|
// 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.
|
// 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 index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||||
const uint8_t *entry = videoRAM + VIDEO_PALETTE_BASE
|
const uint8_t *entry = videoAtlas + VIDEO_PALETTE_BASE
|
||||||
+ index * VIDEO_PALETTE_BYTES;
|
+ index * VIDEO_PALETTE_BYTES;
|
||||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||||
out[0] = entry[0];
|
out[0] = entry[0];
|
||||||
|
|||||||
+54
-11
@@ -19,16 +19,31 @@
|
|||||||
// bits: an 8x8 cell is 64 pixels and each one picks independently out of 256 colours, with
|
// bits: an 8x8 cell is 64 pixels and each one picks independently out of 256 colours, with
|
||||||
// no per-cell limit of the kind that made a Spectrum two and C64 multicolour four.
|
// no per-cell limit of the kind that made a Spectrum two and C64 multicolour four.
|
||||||
//
|
//
|
||||||
// ---- The device brings memory ----
|
// ---- The device brings memory, in two banks ----
|
||||||
//
|
//
|
||||||
// One bank, registered the way the disk's buffer is, so it costs a program nothing in Data
|
// Registered the way the disk's buffer is, so the screen costs a program nothing in Data
|
||||||
// Memory and keeps what is in it between frames. A program blits the region that changed
|
// Memory and keeps what is in it between frames. A program blits the region that changed
|
||||||
// and the rest stays as it was, which is the whole reason this is a bank rather than a
|
// and the rest stays as it was, which is the whole reason this is memory rather than a
|
||||||
// window onto a port.
|
// window onto a port.
|
||||||
|
//
|
||||||
|
// TWO BANKS AND NOT ONE, because the two halves of a screen are written at completely
|
||||||
|
// different rates. Tiles and colours are an ATLAS: put there when a program loads and then
|
||||||
|
// left alone. The map is a SCREEN: rewritten as often as anything moves. Sharing one bank
|
||||||
|
// made them compete for 64K they did not both need, and it had a worse consequence than
|
||||||
|
// being cramped - a bitmap took the whole bank, so ENTERING BITMAP MODE DESTROYED THE FONT.
|
||||||
|
// A program could not draw a picture and then say anything about it.
|
||||||
|
//
|
||||||
|
// Split, each gets a whole 64K and neither can tread on the other. A bitmap now overwrites
|
||||||
|
// the map, which is the same memory meaning a different thing in a different mode and is
|
||||||
|
// exactly what it should overwrite. The tiles behind the text survive it.
|
||||||
|
//
|
||||||
|
// Each bank is the same size, and every address below says which of the two it is in.
|
||||||
#define VIDEO_MEMORY_BYTES 0x10000
|
#define VIDEO_MEMORY_BYTES 0x10000
|
||||||
|
|
||||||
// Tile memory: 256 tiles of 8x8, one byte a pixel.
|
// ---- In the ATLAS bank ----
|
||||||
|
//
|
||||||
|
// Tile memory: 256 tiles of 8x8, one byte a pixel. Everything above it is free, and is
|
||||||
|
// where more tiles and the sprite table are going.
|
||||||
#define VIDEO_TILE_BASE 0x0000
|
#define VIDEO_TILE_BASE 0x0000
|
||||||
#define VIDEO_TILE_BYTES 64
|
#define VIDEO_TILE_BYTES 64
|
||||||
#define VIDEO_TILE_COUNT 256
|
#define VIDEO_TILE_COUNT 256
|
||||||
@@ -43,6 +58,11 @@
|
|||||||
//
|
//
|
||||||
// It also frees the geometry from having to be a power of two, which is what lets the
|
// It also frees the geometry from having to be a power of two, which is what lets the
|
||||||
// pixel resolution be whatever looks right.
|
// pixel resolution be whatever looks right.
|
||||||
|
//
|
||||||
|
// IN THE SCREEN BANK, and still at 0x4000 rather than at the bottom of a bank it now has to
|
||||||
|
// itself. Moving it would have been tidier and would have meant changing which bank a
|
||||||
|
// program registers AND which address it writes to in the same breath - so a screen that
|
||||||
|
// came out wrong would have had two possible causes. The address costs nothing where it is.
|
||||||
#define VIDEO_MAP_BASE 0x4000
|
#define VIDEO_MAP_BASE 0x4000
|
||||||
#define VIDEO_MAP_STRIDE 256
|
#define VIDEO_MAP_STRIDE 256
|
||||||
#define VIDEO_MAP_ROWS 128
|
#define VIDEO_MAP_ROWS 128
|
||||||
@@ -59,8 +79,12 @@
|
|||||||
// tiles rather than on top of them would need a second bank for no reason except tidiness.
|
// 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
|
// 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
|
// screen; it stops calling it a text screen. Coming back finds the map holding whatever the
|
||||||
// whatever the picture put there, which is what taking the screen means.
|
// picture put there, which is what taking the screen means.
|
||||||
|
//
|
||||||
|
// IN THE SCREEN BANK, so what a picture costs is the map and nothing else. THE TILES AND THE
|
||||||
|
// PALETTE ARE IN THE OTHER BANK AND SURVIVE IT, which is what lets a program draw a picture
|
||||||
|
// and then put text back on the screen without reloading the character generator first.
|
||||||
#define VIDEO_BITMAP_BASE 0x0000
|
#define VIDEO_BITMAP_BASE 0x0000
|
||||||
#define VIDEO_BITMAP_WIDTH 320
|
#define VIDEO_BITMAP_WIDTH 320
|
||||||
#define VIDEO_BITMAP_HEIGHT 200
|
#define VIDEO_BITMAP_HEIGHT 200
|
||||||
@@ -71,9 +95,12 @@
|
|||||||
// begins at n times four, which is a shift. Three would need a multiply the machine does
|
// 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.
|
// not have. The fourth byte is unused and reads as whatever was put there.
|
||||||
//
|
//
|
||||||
// At the TOP of video memory, clear of everything else, because it is the one thing that has
|
// At the TOP of the ATLAS bank, clear of everything else, because it is the one thing that
|
||||||
// to mean the same in every mode - a bitmap needs colours as much as a tile does, and 64,000
|
// has to mean the same in every mode - a bitmap needs colours as much as a tile does, and it
|
||||||
// bytes of picture leaves nowhere in the middle for it to hide.
|
// is written when a program loads rather than per frame, which is what the atlas is for.
|
||||||
|
//
|
||||||
|
// Being out of the screen bank is what leaves a bitmap the WHOLE of one: 64,000 bytes of
|
||||||
|
// picture in 65,536, with nothing it has to dodge.
|
||||||
#define VIDEO_PALETTE_BASE 0xFC00
|
#define VIDEO_PALETTE_BASE 0xFC00
|
||||||
#define VIDEO_PALETTE_BYTES 4
|
#define VIDEO_PALETTE_BYTES 4
|
||||||
#define VIDEO_PALETTE_SIZE 256
|
#define VIDEO_PALETTE_SIZE 256
|
||||||
@@ -149,6 +176,19 @@ int videoTextRows(void);
|
|||||||
// Copy the sixteen ink and paper pairs back, leaving the rest of the palette alone.
|
// Copy the sixteen ink and paper pairs back, leaving the rest of the palette alone.
|
||||||
#define VIDEO_COMMAND_PALETTE 0x02
|
#define VIDEO_COMMAND_PALETTE 0x02
|
||||||
|
|
||||||
|
// ---- The port that owns the screen bank ----
|
||||||
|
//
|
||||||
|
// A bank is registered by naming THE PORT THAT OWNS IT, which the controller settled long
|
||||||
|
// before the screen had two of them. So a device with two banks needs two ports that own
|
||||||
|
// memory, and needs no new mechanism at all: the base port owns the atlas, and this one
|
||||||
|
// owns the screen.
|
||||||
|
//
|
||||||
|
// The base port keeps the atlas rather than the screen because tiles have been at 0x0000
|
||||||
|
// since there was a screen at all, and whichever way round this went, one of the two
|
||||||
|
// meanings had to move. Nothing is READ OR WRITTEN here - it is a name for a bank, and the
|
||||||
|
// registry is where a program finds out it brings one.
|
||||||
|
#define VIDEO_SCREEN 0x3A
|
||||||
|
|
||||||
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
||||||
#define VIDEO_FINE_MASK 0x07
|
#define VIDEO_FINE_MASK 0x07
|
||||||
|
|
||||||
@@ -225,7 +265,10 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute);
|
|||||||
// bottom - which is holding whatever was there 128 rows ago, since the map is a ring.
|
// bottom - which is holding whatever was there 128 rows ago, since the map is a ring.
|
||||||
void videoScrollUp(void);
|
void videoScrollUp(void);
|
||||||
|
|
||||||
uint8_t *videoMemory(uint32_t *capacity);
|
// The memory behind one of the device's two ports: VIDEO_STATUS for the atlas, VIDEO_SCREEN
|
||||||
|
// for the screen. NULL for any other port, because the rest of the block owns no memory and
|
||||||
|
// registering a bank onto one would put a number in the table that leads nowhere.
|
||||||
|
uint8_t *videoMemory(uint8_t port, uint32_t *capacity);
|
||||||
|
|
||||||
uint8_t videoWrite(uint8_t value, uint8_t port);
|
uint8_t videoWrite(uint8_t value, uint8_t port);
|
||||||
uint8_t videoRead(uint8_t port);
|
uint8_t videoRead(uint8_t port);
|
||||||
|
|||||||
@@ -615,18 +615,41 @@ It follows that colour depth is free. The map is the same size whatever is behin
|
|||||||
|
|
||||||
### Video Memory:
|
### Video Memory:
|
||||||
|
|
||||||
One bank, brought by the device and reached only through the memory controller, like the disk's buffer. It keeps what is in it between frames, so a program writes the part that changed and the rest stays as it was.
|
**Two banks**, brought by the device and reached only through the memory controller, like the disk's buffer. They keep what is in them between frames, so a program writes the part that changed and the rest stays as it was.
|
||||||
|
|
||||||
| Address | Holds |
|
Two rather than one because the two halves of a screen are written at completely different rates. The **atlas** is tiles and colours: put there when a program loads and then left alone. The **screen** is the map: rewritten as often as anything moves.
|
||||||
|
|
||||||
|
| 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 | 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. |
|
||||||
|
| Screen | 0x0000 - 0xF9FF | In bitmap mode, the picture instead: 64,000 bytes, one to a pixel. |
|
||||||
|
|
||||||
|
**The bitmap is the same memory as the map**, which is what shared video memory has always been. Going to bitmap mode does not clear the text screen - it stops calling it one, and coming back finds the map holding whatever the picture put there.
|
||||||
|
|
||||||
|
**What a picture no longer costs is the font.** The tiles and the palette are in the other bank, where a bitmap cannot reach them, so a program can draw a picture and then put readable text back on the screen without asking the character generator for its glyphs again. While the two shared a bank, drawing anything destroyed them.
|
||||||
|
|
||||||
|
The palette is in the atlas, at the top and out of the way, because it is written when a program loads rather than per frame - and because being out of the screen bank is what leaves a bitmap the whole of one.
|
||||||
|
|
||||||
|
Which bank is which is a property of the **address**, never of the mode: tiles and the palette are always in the atlas, the map and a bitmap always in the screen.
|
||||||
|
|
||||||
|
### Naming Them:
|
||||||
|
|
||||||
|
A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory. The screen has them:
|
||||||
|
|
||||||
|
| Port | Owns |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0x0000 - 0x3FFF | Tile memory. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
|
| 0x30 | The atlas. |
|
||||||
| 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
|
| 0x3A | The screen. |
|
||||||
| 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.
|
Nothing is read or written at 0x3A - it is a name for a bank, and the bus registry is where a program finds out it brings one. Asking the registry about the block gives an honest answer: those two ports say they bring memory and the other fourteen say they do not.
|
||||||
|
|
||||||
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.
|
The base port owns the atlas because tiles have been at 0x0000 since there was a screen at all. `Programs/Examples/picture.asm` registers both, and is the shortest thing to read that does.
|
||||||
|
|
||||||
|
**Registering the second one moves DestBank**, which is worth saying because it is easy to be caught by: RegisterBank takes the number being handed out in DestBank, so a program that registers two banks and then writes without setting DestBank again writes into the second one.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -653,6 +676,8 @@ The high nibble is reserved and should be left at zero, so that a meaning can be
|
|||||||
| 0x36 | Scroll column. Which of the map's 128 columns is drawn at the left. |
|
| 0x36 | Scroll column. Which of the map's 128 columns is drawn at the left. |
|
||||||
| 0x37 | Fine X. How many pixels into that column the screen begins, 0 to 7. |
|
| 0x37 | Fine X. How many pixels into that column the screen begins, 0 to 7. |
|
||||||
| 0x38 | Fine Y. How many pixels into that row the screen begins, 0 to 7. |
|
| 0x38 | Fine Y. How many pixels into that row the screen begins, 0 to 7. |
|
||||||
|
| 0x39 | Command. Bit 0 copies the font back, bit 1 the sixteen colour schemes. |
|
||||||
|
| 0x3A | Owns the screen bank. Not read or written. |
|
||||||
|
|
||||||
| Mode | Screen | Cells |
|
| Mode | Screen | Cells |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 559
|
Grid.sbx 571
|
||||||
Press.sbx 872
|
Press.sbx 872
|
||||||
Mode.sbx 48
|
Mode.sbx 48
|
||||||
Crash.sbx 632
|
Crash.sbx 632
|
||||||
|
|||||||
+89
-44
@@ -56,7 +56,13 @@ start:
|
|||||||
INIA 0x30
|
INIA 0x30
|
||||||
OUTA 0xE2
|
OUTA 0xE2
|
||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8 ; Video memory becomes bank 3
|
OUTA 0xE8 ; The atlas - tiles and palette - becomes bank 3
|
||||||
|
INIA 0d4
|
||||||
|
OUTA 0xE3
|
||||||
|
INIA 0x3A
|
||||||
|
OUTA 0xE2
|
||||||
|
INIA 0x03
|
||||||
|
OUTA 0xE8 ; And the screen - the map, or a bitmap - becomes bank 4
|
||||||
ASM
|
ASM
|
||||||
# ---- Said rather than assumed ----
|
# ---- Said rather than assumed ----
|
||||||
#
|
#
|
||||||
@@ -64,20 +70,30 @@ ASM
|
|||||||
# run, so palette entry 0 is the console's paper rather than black. A check that wanted
|
# 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
|
# 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.
|
# tests are about the device, so they set what they are about to look at.
|
||||||
poke 0xFC00 0x00; poke 0xFC01 0x00; poke 0xFC02 0x00
|
pokeAtlas 0xFC00 0x00; pokeAtlas 0xFC01 0x00; pokeAtlas 0xFC02 0x00
|
||||||
}
|
}
|
||||||
|
|
||||||
poke() {
|
# ---- Which memory, said and not guessed ----
|
||||||
# poke <address> <byte>
|
#
|
||||||
printf ' INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n OUTA 0xE9\n' \
|
# The screen is two banks, and an address alone cannot say which one it means: tile 5 and
|
||||||
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
|
# bitmap pixel 5 are both address 0x0005. So every write below names the memory it is for,
|
||||||
|
# and there is deliberately no bare poke that picks by address - a helper that guessed would
|
||||||
|
# be right for the tiles and wrong for a picture, silently.
|
||||||
|
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.
|
||||||
|
pokeScreen() { pokeTo 4 "$1" "$2"; } # The map, or a bitmap.
|
||||||
|
|
||||||
# Many bytes from one address, using the Data port's own stepping rather than naming the
|
# Many bytes from one address, using the Data port's own stepping rather than naming the
|
||||||
# address again for each. What a run of tile memory is for.
|
# address again for each. What a run of tile memory is for, and so far only the atlas needs
|
||||||
pokeRun() {
|
# one.
|
||||||
# pokeRun <address> <byte> <count>
|
pokeAtlasRun() {
|
||||||
printf ' INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n' \
|
# pokeAtlasRun <address> <byte> <count>
|
||||||
|
printf ' INIA 0d3\n OUTA 0xE3\n INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n' \
|
||||||
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
|
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
|
||||||
local i
|
local i
|
||||||
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
|
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
|
||||||
@@ -214,10 +230,10 @@ 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
|
# 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.
|
# wrong, so the check is where it is AND where it is not.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00
|
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke 0x4000 0x01; poke 0x4001 0x00
|
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||||
poke $((0x4000 + 2 * 256 + 3 * 2)) 0x01
|
pokeScreen $((0x4000 + 2 * 256 + 3 * 2)) 0x01
|
||||||
epilogue
|
epilogue
|
||||||
} | run corner || exit 1
|
} | run corner || exit 1
|
||||||
|
|
||||||
@@ -239,9 +255,9 @@ echo "Checking what the video device draws."
|
|||||||
# Same tile, same map, a different palette entry. Nothing about the picture changes except
|
# Same tile, same map, a different palette entry. Nothing about the picture changes except
|
||||||
# the three bytes the colour came from.
|
# the three bytes the colour came from.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0x00; poke 0xFC05 0xFF; poke 0xFC06 0x40
|
pokeAtlas 0xFC04 0x00; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0x40
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke 0x4000 0x01; poke 0x4001 0x00
|
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||||
epilogue
|
epilogue
|
||||||
} | run palette || exit 1
|
} | run palette || exit 1
|
||||||
|
|
||||||
@@ -255,11 +271,11 @@ echo "Checking what the video device draws."
|
|||||||
# the only difference between the two cells is the attribute nibble: 0 leaves the index
|
# 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.
|
# alone, 1 adds sixteen. This is the whole of the recolouring feature in one check.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00
|
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||||
poke 0xFC44 0x00; poke 0xFC45 0x00; poke 0xFC46 0xFF
|
pokeAtlas 0xFC44 0x00; pokeAtlas 0xFC45 0x00; pokeAtlas 0xFC46 0xFF
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke 0x4000 0x01; poke 0x4001 0x00
|
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||||
poke 0x4002 0x01; poke 0x4003 0x01
|
pokeScreen 0x4002 0x01; pokeScreen 0x4003 0x01
|
||||||
epilogue
|
epilogue
|
||||||
} | run attribute || exit 1
|
} | run attribute || exit 1
|
||||||
|
|
||||||
@@ -276,9 +292,9 @@ 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
|
# row to the top of the screen, which is the whole reason a terminal on this machine is
|
||||||
# affordable at all.
|
# affordable at all.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0xFF; poke 0xFC05 0xFF; poke 0xFC06 0x00
|
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0x00
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke $((0x4000 + 3 * 256)) 0x01
|
pokeScreen $((0x4000 + 3 * 256)) 0x01
|
||||||
port 0x34 0x03
|
port 0x34 0x03
|
||||||
epilogue
|
epilogue
|
||||||
} | run scroll || exit 1
|
} | run scroll || exit 1
|
||||||
@@ -295,9 +311,9 @@ 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.
|
# 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.
|
# A map that clipped instead of wrapping would show nothing on the second row.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0xFF; poke 0xFC05 0xFF; poke 0xFC06 0xFF
|
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0xFF
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke 0x4000 0x01
|
pokeScreen 0x4000 0x01
|
||||||
port 0x34 0x7F
|
port 0x34 0x7F
|
||||||
epilogue
|
epilogue
|
||||||
} | run ring || exit 1
|
} | run ring || exit 1
|
||||||
@@ -553,8 +569,8 @@ coloured blinkagain 0 0 "216,216,216" \
|
|||||||
|
|
||||||
# Palette entry 5, then one pixel of it at row 2, column 3 - which is byte 2*320+3 = 643.
|
# Palette entry 5, then one pixel of it at row 2, column 3 - which is byte 2*320+3 = 643.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90
|
pokeAtlas 0xFC14 0x20; pokeAtlas 0xFC15 0xC0; pokeAtlas 0xFC16 0x90
|
||||||
poke 0x0283 0x05
|
pokeScreen 0x0283 0x05
|
||||||
port 0x31 0x02
|
port 0x31 0x02
|
||||||
epilogue
|
epilogue
|
||||||
} | run bitmap || exit 1
|
} | run bitmap || exit 1
|
||||||
@@ -575,8 +591,8 @@ coloured bitmap 4 2 "0,0,0" \
|
|||||||
# somebody's picture with marks nobody can read. It still says everything down the serial
|
# somebody's picture with marks nobody can read. It still says everything down the serial
|
||||||
# line, which is where it was going as well.
|
# line, which is where it was going as well.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90
|
pokeAtlas 0xFC14 0x20; pokeAtlas 0xFC15 0xC0; pokeAtlas 0xFC16 0x90
|
||||||
poke 0x0283 0x05
|
pokeScreen 0x0283 0x05
|
||||||
port 0x31 0x02
|
port 0x31 0x02
|
||||||
say "A"
|
say "A"
|
||||||
epilogue
|
epilogue
|
||||||
@@ -596,6 +612,35 @@ coloured bitmaptext 3 2 "32,192,144" \
|
|||||||
&& result ok "a bitmap has no columns" "and forty again when it is text" \
|
&& result ok "a bitmap has no columns" "and forty again when it is text" \
|
||||||
|| result no "a bitmap has no columns" "got $(said bitmapsize)"
|
|| result no "a bitmap has no columns" "got $(said bitmapsize)"
|
||||||
|
|
||||||
|
# ---- The example that draws one, run as it ships ----
|
||||||
|
#
|
||||||
|
# Everything above builds its program here, which means every check above passes on an
|
||||||
|
# emulator whose two banks are wired up EXACTLY the way this file assumes. picture.asm is the
|
||||||
|
# thing somebody reads to learn how to draw, and nothing ran it.
|
||||||
|
#
|
||||||
|
# That is not hypothetical. Splitting video memory into two banks broke this program and no
|
||||||
|
# check noticed, because registering the second bank leaves DestBank pointing at it - so the
|
||||||
|
# palette went into the screen instead of the atlas and the picture came out black.
|
||||||
|
#
|
||||||
|
# What is checked is the gradient the program's own comment promises: two hundred rows, each
|
||||||
|
# one colour, running blue to white to yellow. A blank screen has one colour and a picture
|
||||||
|
# drawn with the wrong palette has a handful, so counting them catches both.
|
||||||
|
"$ASM" -I "$ROOT/Programs/Libraries" "$ROOT/Programs/Examples/picture.asm" \
|
||||||
|
-o "$BUILD/picture.bin" > "$BUILD/picture.log" 2>&1
|
||||||
|
timeout 30 "$EMU" --fast --cycles 5000000 --screen "$BUILD/picture.ppm" \
|
||||||
|
"$BUILD/picture.bin" > "$BUILD/picture.out" 2>&1 || true
|
||||||
|
SHADES="$(python3 -c "
|
||||||
|
d = open('$BUILD/picture.ppm', 'rb').read()
|
||||||
|
px = d[d.index(b'255\n') + 4:]
|
||||||
|
print(len({px[o:o + 3] for o in range(0, len(px), 3)}))
|
||||||
|
" 2>/dev/null || echo 0)"
|
||||||
|
[ "$SHADES" = "200" ] \
|
||||||
|
&& result ok "the example draws its picture" "two hundred rows, two hundred colours" \
|
||||||
|
|| result no "the example draws its picture" "$SHADES colours, not 200"
|
||||||
|
[ "$(pixel picture 10 0)" = "0,0,255" ] && [ "$(pixel picture 10 199)" = "199,199,56" ] \
|
||||||
|
&& result ok "and it runs blue to yellow" "the palette is in the atlas, where it belongs" \
|
||||||
|
|| result no "and it runs blue to yellow" "top $(pixel picture 10 0), bottom $(pixel picture 10 199)"
|
||||||
|
|
||||||
# ---- The frame, which is the only beat this machine has ----
|
# ---- 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
|
# There is no clock. Every program that wanted to happen at a certain speed has until now
|
||||||
@@ -735,9 +780,9 @@ TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/poller.out" | grep -oE '[0-9]+')"
|
|||||||
# what is compared is where the red stops.
|
# what is compared is where the red stops.
|
||||||
scrollSetup() {
|
scrollSetup() {
|
||||||
prologue
|
prologue
|
||||||
poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00
|
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||||
poke 0x4000 0x01; poke 0x4001 0x00
|
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||||
}
|
}
|
||||||
|
|
||||||
# Where it is with nothing scrolled: the red runs from 0 to 7 and stops.
|
# Where it is with nothing scrolled: the red runs from 0 to 7 and stops.
|
||||||
@@ -767,14 +812,14 @@ scrollSetup() {
|
|||||||
|
|
||||||
# Coarse X moves a whole cell. With the column origin at 1 the corner cell is off the left
|
# Coarse X moves a whole cell. With the column origin at 1 the corner cell is off the left
|
||||||
# and cell 1 of the map is where the screen starts - so the corner is no longer red.
|
# and cell 1 of the map is where the screen starts - so the corner is no longer red.
|
||||||
{ scrollSetup; poke $((0x4000 + 2)) 0x01; port 0x36 0x01; epilogue; } | run scrollcx || exit 1
|
{ scrollSetup; pokeScreen $((0x4000 + 2)) 0x01; port 0x36 0x01; epilogue; } | run scrollcx || exit 1
|
||||||
[ "$(pixel scrollcx 0 0)" = "255,0,0" ] && [ "$(pixel scrollcx 8 0)" != "255,0,0" ] \
|
[ "$(pixel scrollcx 0 0)" = "255,0,0" ] && [ "$(pixel scrollcx 8 0)" != "255,0,0" ] \
|
||||||
&& result ok "coarse X moves a whole cell" "the map moved one cell left" \
|
&& result ok "coarse X moves a whole cell" "the map moved one cell left" \
|
||||||
|| result no "coarse X moves a whole cell" "0 is $(pixel scrollcx 0 0), 8 is $(pixel scrollcx 8 0)"
|
|| result no "coarse X moves a whole cell" "0 is $(pixel scrollcx 0 0), 8 is $(pixel scrollcx 8 0)"
|
||||||
|
|
||||||
# And it is a ring, the same as the rows are. Column 127 is the last one a map row has, so
|
# And it is a ring, the same as the rows are. Column 127 is the last one a map row has, so
|
||||||
# an origin there puts it on screen with column 0 beside it.
|
# an origin there puts it on screen with column 0 beside it.
|
||||||
{ scrollSetup; poke $((0x4000 + 127 * 2)) 0x01; port 0x36 0x7F; epilogue; } | run scrollwrapx || exit 1
|
{ scrollSetup; pokeScreen $((0x4000 + 127 * 2)) 0x01; port 0x36 0x7F; epilogue; } | run scrollwrapx || exit 1
|
||||||
[ "$(pixel scrollwrapx 0 0)" = "255,0,0" ] && [ "$(pixel scrollwrapx 8 0)" = "255,0,0" ] \
|
[ "$(pixel scrollwrapx 0 0)" = "255,0,0" ] && [ "$(pixel scrollwrapx 8 0)" = "255,0,0" ] \
|
||||||
&& result ok "the columns are a ring too" "127 on screen with 0 beside it" \
|
&& result ok "the columns are a ring too" "127 on screen with 0 beside it" \
|
||||||
|| result no "the columns are a ring too" "0 is $(pixel scrollwrapx 0 0), 8 is $(pixel scrollwrapx 8 0)"
|
|| result no "the columns are a ring too" "0 is $(pixel scrollwrapx 0 0), 8 is $(pixel scrollwrapx 8 0)"
|
||||||
@@ -934,7 +979,7 @@ inked scrollback 2 1 \
|
|||||||
# The glyph for 'A' is filled with ink, which makes the cell a solid block, so the top left
|
# The glyph for 'A' is filled with ink, which makes the cell a solid block, so the top left
|
||||||
# pixel of it is ink where a real 'A' has paper. That is a pixel no font disagrees about.
|
# pixel of it is ink where a real 'A' has paper. That is a pixel no font disagrees about.
|
||||||
{ prologue
|
{ prologue
|
||||||
pokeRun 0x0840 0x01 64 # Tile 33, which is 'A', every pixel ink.
|
pokeAtlasRun 0x0840 0x01 64 # Tile 33, which is 'A', every pixel ink.
|
||||||
say "A"
|
say "A"
|
||||||
epilogue
|
epilogue
|
||||||
} | run fontwrecked || exit 1
|
} | run fontwrecked || exit 1
|
||||||
@@ -943,7 +988,7 @@ inked scrollback 2 1 \
|
|||||||
|| result no "a program can overwrite a glyph" "not ink at 0,0"
|
|| result no "a program can overwrite a glyph" "not ink at 0,0"
|
||||||
|
|
||||||
{ prologue
|
{ prologue
|
||||||
pokeRun 0x0840 0x01 64
|
pokeAtlasRun 0x0840 0x01 64
|
||||||
port 0x39 0x01 # And ask the character generator for it back.
|
port 0x39 0x01 # And ask the character generator for it back.
|
||||||
say "A"
|
say "A"
|
||||||
epilogue
|
epilogue
|
||||||
@@ -959,9 +1004,9 @@ inked scrollback 2 1 \
|
|||||||
# that defined a tile of its own and then wanted its text back would have paid for it with
|
# that defined a tile of its own and then wanted its text back would have paid for it with
|
||||||
# the tile. It writes the glyphs it has and stops.
|
# the tile. It writes the glyphs it has and stops.
|
||||||
{ prologue
|
{ prologue
|
||||||
pokeRun 0x3200 0x01 64 # Tile 200, well above anything the font occupies.
|
pokeAtlasRun 0x3200 0x01 64 # Tile 200, well above anything the font occupies.
|
||||||
poke 0x4000 0xC8 # And that tile in the first cell of the map.
|
pokeScreen 0x4000 0xC8 # And that tile in the first cell of the map.
|
||||||
poke 0x4001 0x00
|
pokeScreen 0x4001 0x00
|
||||||
port 0x39 0x03 # Both the font and the palette back.
|
port 0x39 0x03 # Both the font and the palette back.
|
||||||
epilogue
|
epilogue
|
||||||
} | run fontkeeps || exit 1
|
} | run fontkeeps || exit 1
|
||||||
@@ -975,7 +1020,7 @@ inked scrollback 2 1 \
|
|||||||
# which is exactly what the fault screen has to survive. The device is asked for the sixteen
|
# which is exactly what the fault screen has to survive. The device is asked for the sixteen
|
||||||
# schemes back and the writing returns.
|
# schemes back and the writing returns.
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0x00; poke 0xFC05 0x00; poke 0xFC06 0x00 # Scheme 0's ink, made black.
|
pokeAtlas 0xFC04 0x00; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 # Scheme 0's ink, made black.
|
||||||
say "A"
|
say "A"
|
||||||
epilogue
|
epilogue
|
||||||
} | run inkwrecked || exit 1
|
} | run inkwrecked || exit 1
|
||||||
@@ -984,7 +1029,7 @@ inked scrollback 2 1 \
|
|||||||
|| result no "a program can overwrite a scheme" "the A is still visible"
|
|| result no "a program can overwrite a scheme" "the A is still visible"
|
||||||
|
|
||||||
{ prologue
|
{ prologue
|
||||||
poke 0xFC04 0x00; poke 0xFC05 0x00; poke 0xFC06 0x00
|
pokeAtlas 0xFC04 0x00; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||||
port 0x39 0x02 # The schemes back, and only the schemes.
|
port 0x39 0x02 # The schemes back, and only the schemes.
|
||||||
say "A"
|
say "A"
|
||||||
epilogue
|
epilogue
|
||||||
|
|||||||
Reference in New Issue
Block a user