A window: a layer that does not scroll

The map moves and this does not, which the map alone cannot express. The
scroll registers move ALL of it, so a score printed into the map slides
away, and one printed into whichever rows the view happens to be showing
jumps a pixel at a time as the fine offset changes.

Port 0x3D is how many rows tall and 0x3E is which row it starts at. Nought
tall is no window, so a cleared screen has none and every program written
before this means what it meant. A start row is a register because a
status bar along the bottom is as common as one along the top.

IT HAS ITS OWN MEMORY, and that is the argument for it. The cheaper design
draws the top rows of the MAP without the scroll applied - no new memory,
one register - and makes those rows part of the playfield's ring, so a
game that scrolls vertically has to route its world around its own
scoreboard for ever. The point of a status bar is that it is not somewhere
in the level. Lunar Porter does not scroll vertically today and will the
moment an orbit is a thing you can reach.

0xC000 in the screen bank, which the map does not reach: it ends at
0xBFFF. Same cells, same tiles, same pages, same schemes. Being in the
screen bank makes it per screen, so flipping the buffer flips the status
bar with it - what a double buffered game wants, and surprising the other
way round.

Drawn over everything, sprites included. A sprite that could cover the
fuel gauge would be a bug in every game that had both.

Tile modes only. In bitmap mode the picture is using that memory, so a
bitmap program pins things to the screen with sprites, which are in screen
coordinates for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-03 00:19:09 -04:00
co-authored by Claude Opus 5
parent e3eccd17a8
commit 17c8da111f
4 changed files with 194 additions and 0 deletions
+78
View File
@@ -202,6 +202,15 @@ pageTile() {
pokeAtlasRun "$(( $1 * 0x4000 + $2 * 64 ))" "$3" 64
}
# One cell of the window layer, which is 0xC000 in the screen bank and a page a row, exactly
# as the map is - the same cells, and never anywhere near the map's own rows.
pokeWindow() {
# pokeWindow <row> <column> <tile> <attribute>
local at=$(( 0xC000 + $1 * 256 + $2 * 2 ))
pokeScreen "$at" "$3"
pokeScreen "$(( at + 1 ))" "$4"
}
# The top half one index and the bottom half another, which is how a tile gets a hole in it:
# index nought is what a sprite does not draw.
halfTile() {
@@ -745,6 +754,75 @@ echo "Checking what the video device draws."
&& result ok "a size past the screen is clipped" "it filled the screen and stopped there" \
|| result no "a size past the screen is clipped" "$(pixel spritehuge 0 0) $(pixel spritehuge 319 199)"
# ---- A window, which does not scroll ----
#
# The map is scrolled five rows and three pixels, and the window cell has to come out at the
# same place it would with neither. That is the entire feature: a window cell is at a SCREEN
# position, where a map cell is at a position in a world the screen is looking at part of.
{ prologue
spriteColours
solidTile 1 0x01
pokeWindow 0 2 0x01 0x00
port 0x3D 0x01 # One row tall.
port 0x3E 0x00 # At the top.
port 0x34 0x05 # And the map five rows down and three pixels into a cell.
port 0x38 0x03
epilogue
} | run window || exit 1
[ "$(pixel window 16 0)" = "255,0,0" ] && [ "$(pixel window 23 7)" = "255,0,0" ] \
&& result ok "a window cell sits where the screen is" "the scroll registers did not move it" \
|| result no "a window cell sits where the screen is" "$(pixel window 16 0) and $(pixel window 23 7)"
[ "$(pixel window 24 0)" = "0,0,0" ] \
&& result ok "and stops where it ends" "one cell wide, and the next is not it" \
|| result no "and stops where it ends" "got $(pixel window 24 0)"
# ---- Where it starts is its own register ----
#
# A status bar along the bottom is as common as one along the top, and working the row out
# from the screen height is a sum every program would otherwise do again.
{ prologue
spriteColours
solidTile 1 0x01
pokeWindow 0 2 0x01 0x00
port 0x3D 0x01
port 0x3E 10 # Ten rows down, which is eighty pixels. Shell arithmetic, so
# ten and not 0d10 - that is the assembler's notation and this
# is a printf away from being an INIA.
epilogue
} | run windowat || exit 1
[ "$(pixel windowat 16 80)" = "255,0,0" ] && [ "$(pixel windowat 16 0)" = "0,0,0" ] \
&& result ok "and it starts where it is told" "row ten, and nothing at the top" \
|| result no "and it starts where it is told" "at 80 $(pixel windowat 16 80), at 0 $(pixel windowat 16 0)"
# ---- Over everything, sprites included ----
#
# A sprite that could cover the fuel gauge would be a bug in every game that had both.
{ prologue
spriteColours
solidTile 1 0x01; solidTile 2 0x02
pokeWindow 0 2 0x01 0x00
spriteAt 0 2 0x00 16 0 0x11 0x00
port 0x3D 0x01
epilogue
} | run windowover || exit 1
[ "$(pixel windowover 16 0)" = "255,0,0" ] \
&& result ok "a window covers a sprite" "the bar wins, which is what a bar is for" \
|| result no "a window covers a sprite" "got $(pixel windowover 16 0)"
# ---- And none of it happens until it is asked for ----
#
# Nought rows is no window, so a cleared screen has none and every program written before this
# existed means what it meant. The cell is written and the height is not set.
{ prologue
spriteColours
solidTile 1 0x01
pokeWindow 0 2 0x01 0x00
epilogue
} | run windowoff || exit 1
[ "$(pixel windowoff 16 0)" = "0,0,0" ] \
&& result ok "no height is no window" "written, and not drawn" \
|| result no "no height is no window" "got $(pixel windowoff 16 0)"
# ---- Two screens, and the flip between them ----
#
# One red cell in each screen, in different rows: row one of the screen being shown, and the