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
+89
-44
@@ -56,7 +56,13 @@ start:
|
||||
INIA 0x30
|
||||
OUTA 0xE2
|
||||
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
|
||||
# ---- 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
|
||||
# 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 0xFC00 0x00; poke 0xFC01 0x00; poke 0xFC02 0x00
|
||||
pokeAtlas 0xFC00 0x00; pokeAtlas 0xFC01 0x00; pokeAtlas 0xFC02 0x00
|
||||
}
|
||||
|
||||
poke() {
|
||||
# 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' \
|
||||
$(( ($1 >> 8) & 0xFF )) $(( $1 & 0xFF )) $(( $2 & 0xFF ))
|
||||
# ---- Which memory, said and not guessed ----
|
||||
#
|
||||
# The screen is two banks, and an address alone cannot say which one it means: tile 5 and
|
||||
# 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
|
||||
# address again for each. What a run of tile memory is for.
|
||||
pokeRun() {
|
||||
# pokeRun <address> <byte> <count>
|
||||
printf ' INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n' \
|
||||
# address again for each. What a run of tile memory is for, and so far only the atlas needs
|
||||
# one.
|
||||
pokeAtlasRun() {
|
||||
# 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 ))
|
||||
local i
|
||||
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
|
||||
# wrong, so the check is where it is AND where it is not.
|
||||
{ prologue
|
||||
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
|
||||
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||
pokeScreen $((0x4000 + 2 * 256 + 3 * 2)) 0x01
|
||||
epilogue
|
||||
} | 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
|
||||
# the three bytes the colour came from.
|
||||
{ prologue
|
||||
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
|
||||
pokeAtlas 0xFC04 0x00; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0x40
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||
epilogue
|
||||
} | 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
|
||||
# alone, 1 adds sixteen. This is the whole of the recolouring feature in one check.
|
||||
{ prologue
|
||||
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
|
||||
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||
pokeAtlas 0xFC44 0x00; pokeAtlas 0xFC45 0x00; pokeAtlas 0xFC46 0xFF
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||
pokeScreen 0x4002 0x01; pokeScreen 0x4003 0x01
|
||||
epilogue
|
||||
} | 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
|
||||
# affordable at all.
|
||||
{ prologue
|
||||
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
|
||||
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0x00
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen $((0x4000 + 3 * 256)) 0x01
|
||||
port 0x34 0x03
|
||||
epilogue
|
||||
} | 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.
|
||||
# A map that clipped instead of wrapping would show nothing on the second row.
|
||||
{ prologue
|
||||
poke 0xFC04 0xFF; poke 0xFC05 0xFF; poke 0xFC06 0xFF
|
||||
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
||||
poke 0x4000 0x01
|
||||
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0xFF; pokeAtlas 0xFC06 0xFF
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen 0x4000 0x01
|
||||
port 0x34 0x7F
|
||||
epilogue
|
||||
} | 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.
|
||||
{ prologue
|
||||
poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90
|
||||
poke 0x0283 0x05
|
||||
pokeAtlas 0xFC14 0x20; pokeAtlas 0xFC15 0xC0; pokeAtlas 0xFC16 0x90
|
||||
pokeScreen 0x0283 0x05
|
||||
port 0x31 0x02
|
||||
epilogue
|
||||
} | 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
|
||||
# line, which is where it was going as well.
|
||||
{ prologue
|
||||
poke 0xFC14 0x20; poke 0xFC15 0xC0; poke 0xFC16 0x90
|
||||
poke 0x0283 0x05
|
||||
pokeAtlas 0xFC14 0x20; pokeAtlas 0xFC15 0xC0; pokeAtlas 0xFC16 0x90
|
||||
pokeScreen 0x0283 0x05
|
||||
port 0x31 0x02
|
||||
say "A"
|
||||
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 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 ----
|
||||
#
|
||||
# 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.
|
||||
scrollSetup() {
|
||||
prologue
|
||||
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
|
||||
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00
|
||||
for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done
|
||||
pokeScreen 0x4000 0x01; pokeScreen 0x4001 0x00
|
||||
}
|
||||
|
||||
# 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
|
||||
# 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" ] \
|
||||
&& 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)"
|
||||
|
||||
# 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.
|
||||
{ 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" ] \
|
||||
&& 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)"
|
||||
@@ -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
|
||||
# pixel of it is ink where a real 'A' has paper. That is a pixel no font disagrees about.
|
||||
{ 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"
|
||||
epilogue
|
||||
} | 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"
|
||||
|
||||
{ prologue
|
||||
pokeRun 0x0840 0x01 64
|
||||
pokeAtlasRun 0x0840 0x01 64
|
||||
port 0x39 0x01 # And ask the character generator for it back.
|
||||
say "A"
|
||||
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
|
||||
# the tile. It writes the glyphs it has and stops.
|
||||
{ prologue
|
||||
pokeRun 0x3200 0x01 64 # Tile 200, well above anything the font occupies.
|
||||
poke 0x4000 0xC8 # And that tile in the first cell of the map.
|
||||
poke 0x4001 0x00
|
||||
pokeAtlasRun 0x3200 0x01 64 # Tile 200, well above anything the font occupies.
|
||||
pokeScreen 0x4000 0xC8 # And that tile in the first cell of the map.
|
||||
pokeScreen 0x4001 0x00
|
||||
port 0x39 0x03 # Both the font and the palette back.
|
||||
epilogue
|
||||
} | 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
|
||||
# schemes back and the writing returns.
|
||||
{ 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"
|
||||
epilogue
|
||||
} | 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"
|
||||
|
||||
{ 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.
|
||||
say "A"
|
||||
epilogue
|
||||
|
||||
Reference in New Issue
Block a user