Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were missing and this adds them: a column origin so the map can be wider than the screen as well as taller, and a pixel remainder for each axis so the step can be one pixel rather than eight. 0x36 Scroll column, in cells, wrapping at 128 0x37 Fine X, 0 to 7 pixels 0x38 Fine Y, 0 to 7 pixels FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0, because only its low three bits mean anything. The alternative was for a write of 8 to step the coarse register, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic itself. It is also what the machines this one is pretending to be did. The renderer now draws one more row and one more column than fit and clips them, because with a fine offset the screen no longer begins on a cell boundary and the cells at two edges are partly off it. videoPutCell follows the column origin as it has always followed the row - a caller means a cell of the SCREEN, and the screen is a window onto the map. The fine offsets are deliberately not applied there: they move the finished picture by less than a cell, and there is no such thing as less than a cell to write into. So a program may scroll to any pixel without the console's idea of where row three, column five is moving underneath it. Grid now scrolls diagonally, a pixel a frame, in four port writes and two carries. It moved eight pixels every fourth frame before, which reads as the picture jumping rather than travelling. Seven checks, each one the same program with one register changed, so what is compared is where the picture stopped. Breaking fine X, fine Y, the column origin, the three-bit mask, or the console's use of the origin each fails exactly one of them. Grid's own two checks had to be rewritten, and the reason is worth keeping: they asked whether pixel 4 was a grid line, which was really a check that the scroll happened to be at a cell boundary. A picture that moves a pixel a frame can only be asked things that are true at every offset - that it repeats every eight pixels, and that one band of eight rows holds different colours from the next. Also repairs docs.sh, which found the minimal CosmOS application by taking the first asm block in the README. Documenting a program with an example above it made that a different block, and the check complained that the minimal application had no #Base about something that never claimed to be one. It looks under System Services now. 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
848103f5e4
commit
bcd42e75ca
+9
-1
@@ -510,7 +510,15 @@ else:
|
||||
# The minimal application in the same section is what somebody copies, so it is the
|
||||
# part of the map most worth being right. It went stale across the doubling while the
|
||||
# table above it was corrected.
|
||||
example = re.search(r"```asm\n(.*?)```", readme, re.S)
|
||||
# ---- Found by its section, not by being first ----
|
||||
#
|
||||
# This took the first asm block in the file, which was the minimal application right up
|
||||
# until somebody documented a program with an assembly example above it - and then this
|
||||
# said the minimal application had no #Base, about a block that was never claiming to be
|
||||
# one. The example lives under System Services; that is what identifies it.
|
||||
services = readme.split("### System Services:", 1)
|
||||
example = (re.search(r"```asm\n(.*?)```", services[1], re.S)
|
||||
if len(services) > 1 else None)
|
||||
if not example:
|
||||
problems.append("the CosmOS README no longer shows a minimal application")
|
||||
else:
|
||||
|
||||
@@ -7,7 +7,7 @@ Snake.sbx 2164
|
||||
Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 510
|
||||
Grid.sbx 539
|
||||
notes.txt 21
|
||||
hi.script 121
|
||||
bad.script 45
|
||||
|
||||
@@ -6,7 +6,7 @@ Snake.sbx 2164
|
||||
Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 510
|
||||
Grid.sbx 539
|
||||
notes.txt 21
|
||||
hi.script 121
|
||||
bad.script 45
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Two questions video.sh cannot ask of a picture that moves.
|
||||
#
|
||||
# Grid scrolls a pixel a frame, so nothing can be asked about a particular pixel: whether
|
||||
# pixel 4 is a grid line depends on which frame this is. Both questions below are true of the
|
||||
# picture whatever the fine scroll offset happens to be.
|
||||
#
|
||||
# grid - is this a grid of one repeated tile? Periodic every eight pixels across, and
|
||||
# not all one colour.
|
||||
# bands - does the attribute nibble recolour each cell row? The colours found in one band
|
||||
# of eight rows differ from those in the next, whatever the ground is doing.
|
||||
import sys
|
||||
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
fields = data.split(b"\n", 3)
|
||||
width = int(fields[1].split()[0])
|
||||
body = fields[3]
|
||||
|
||||
|
||||
def pixel(x, y):
|
||||
at = (y * width + x) * 3
|
||||
return tuple(body[at:at + 3])
|
||||
|
||||
|
||||
if sys.argv[2] == "grid":
|
||||
row = [pixel(x, 4) for x in range(32)]
|
||||
periodic = all(row[x] == row[x + 8] for x in range(24))
|
||||
varied = len(set(row)) > 1
|
||||
print("yes" if periodic and varied else
|
||||
"not periodic" if not periodic else "all one colour")
|
||||
else:
|
||||
# A band is eight rows, which is one cell row wherever the boundary has slid to. The
|
||||
# ground is the same under every scheme, so what tells them apart is the line colour -
|
||||
# and looking at the whole band finds it without knowing where in the band it is.
|
||||
first = set(pixel(0, y) for y in range(8))
|
||||
second = set(pixel(0, y) for y in range(8, 16))
|
||||
print("yes" if first != second else "the same in both: %s" % sorted(first))
|
||||
+82
-12
@@ -718,6 +718,71 @@ TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/poller.out" | grep -oE '[0-9]+')"
|
||||
&& result ok "and the flag comes down when looked at" "$TOTAL cycles, so three frames passed" \
|
||||
|| result no "and the flag comes down when looked at" "$TOTAL cycles, too few to be three frames"
|
||||
|
||||
# ---- Scrolling by less than a cell, and sideways ----
|
||||
#
|
||||
# A red tile in the corner and nowhere else, so that where it lands says exactly what the
|
||||
# scroll registers did. Every check below is the SAME program with one register changed, and
|
||||
# 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
|
||||
}
|
||||
|
||||
# Where it is with nothing scrolled: the red runs from 0 to 7 and stops.
|
||||
{ scrollSetup; epilogue; } | run scroll0 || exit 1
|
||||
[ "$(pixel scroll0 7 0)" = "255,0,0" ] && [ "$(pixel scroll0 8 0)" != "255,0,0" ] \
|
||||
&& result ok "the tile ends at the cell edge" "red from 0 to 7" \
|
||||
|| result no "the tile ends at the cell edge" "7 is $(pixel scroll0 7 0), 8 is $(pixel scroll0 8 0)"
|
||||
|
||||
# One pixel of fine X moves the picture one pixel LEFT: the view slides right, so the red
|
||||
# now ends at 6. One pixel, not eight, is the whole point of the register.
|
||||
{ scrollSetup; port 0x37 0x01; epilogue; } | run scrollfx || exit 1
|
||||
[ "$(pixel scrollfx 6 0)" = "255,0,0" ] && [ "$(pixel scrollfx 7 0)" != "255,0,0" ] \
|
||||
&& result ok "fine X moves it one pixel" "the edge went from 7 to 6" \
|
||||
|| result no "fine X moves it one pixel" "6 is $(pixel scrollfx 6 0), 7 is $(pixel scrollfx 7 0)"
|
||||
|
||||
{ scrollSetup; port 0x38 0x01; epilogue; } | run scrollfy || exit 1
|
||||
[ "$(pixel scrollfy 0 6)" = "255,0,0" ] && [ "$(pixel scrollfy 0 7)" != "255,0,0" ] \
|
||||
&& result ok "and fine Y moves it one pixel" "the edge went from 7 to 6" \
|
||||
|| result no "and fine Y moves it one pixel" "6 is $(pixel scrollfy 0 6), 7 is $(pixel scrollfy 0 7)"
|
||||
|
||||
# Seven is as far as it goes. Eight is zero again and NOT one cell along, which is what "it
|
||||
# does not carry" means where a program can see it.
|
||||
{ scrollSetup; port 0x37 0x08; epilogue; } | run scrollwrap || exit 1
|
||||
[ "$(pixel scrollwrap 7 0)" = "255,0,0" ] && [ "$(pixel scrollwrap 8 0)" != "255,0,0" ] \
|
||||
&& result ok "eight of fine is none of it" "the low three bits, and no carry" \
|
||||
|| result no "eight of fine is none of it" "7 is $(pixel scrollwrap 7 0)"
|
||||
|
||||
# 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
|
||||
[ "$(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
|
||||
[ "$(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)"
|
||||
|
||||
# ---- And the console follows the column origin ----
|
||||
#
|
||||
# It has always followed the row origin, which is where its scrollback comes from. A letter
|
||||
# written while the view is scrolled sideways has to land where the writer meant - on the
|
||||
# screen - and not at the map cell that happens to share its number.
|
||||
{ printf '#Program\nstart:\n'
|
||||
port 0x36 0x03
|
||||
say "A"
|
||||
epilogue
|
||||
} | run scrollconsole || exit 1
|
||||
inked scrollconsole 2 1 \
|
||||
&& result ok "the console writes where it means to" "the letter is in the first cell of the screen" \
|
||||
|| result no "the console writes where it means to" "nothing at 2,1"
|
||||
|
||||
# ---- The tile engine, driven by a program rather than by the console ----
|
||||
#
|
||||
# Everything above drives the screen from a bare test program. This boots the whole system
|
||||
@@ -736,20 +801,25 @@ timeout 30 "$EMU" --fast --cycles 8000000 --keyboard "$BUILD/grid.keys" \
|
||||
"$BUILD/cosmos.bin" > "$BUILD/grid.out" 2>&1 || true
|
||||
|
||||
if [ -f "$BUILD/grid.ppm" ]; then
|
||||
# A cell is eight by eight with a line along its top and down its left, so within one
|
||||
# cell the corner is line and the middle is ground - and the cell to the right starts
|
||||
# with a line again. That is a grid rather than a wash of colour.
|
||||
corner="$(pixel grid 0 0)"; middle="$(pixel grid 4 4)"; nextcell="$(pixel grid 8 4)"
|
||||
[ "$corner" != "$middle" ] && [ "$nextcell" = "$corner" ] \
|
||||
&& result ok "a program drew a grid of its own tile" "line $corner, ground $middle" \
|
||||
|| result no "a program drew a grid of its own tile" "corner $corner middle $middle next $nextcell"
|
||||
# ---- Asked in a way that a moving picture can answer ----
|
||||
#
|
||||
# Not "is pixel 0 a line and pixel 4 the ground", which was the first version and was
|
||||
# really a check that the scroll happened to be at a cell boundary. Grid now moves a pixel
|
||||
# a frame, so where the lines are depends on which frame this is - but a grid of one tile
|
||||
# is PERIODIC whatever the offset: every pixel matches the one eight along. And it is not
|
||||
# all one colour, or a blank screen would pass.
|
||||
GRIDLIKE="$(python3 "$ROOT/Tests/periodic.py" "$BUILD/grid.ppm" grid)"
|
||||
[ "$GRIDLIKE" = "yes" ] \
|
||||
&& result ok "a program drew a grid of its own tile" "the picture repeats every eight pixels" \
|
||||
|| result no "a program drew a grid of its own tile" "not a grid of one tile ($GRIDLIKE)"
|
||||
|
||||
# The attribute nibble adds sixteen to every index in the tile, so consecutive map rows
|
||||
# come out in consecutive schemes. Two cell rows apart must not be the same colour.
|
||||
one="$(pixel grid 0 0)"; two="$(pixel grid 0 8)"
|
||||
[ "$one" != "$two" ] \
|
||||
&& result ok "the attribute nibble recolours it" "row 0 $one, row 1 $two" \
|
||||
|| result no "the attribute nibble recolours it" "both rows are $one"
|
||||
# come out in consecutive schemes. Eight pixels apart is one cell row apart whatever the
|
||||
# fine offset is, so this one survives the scrolling too.
|
||||
BANDED="$(python3 "$ROOT/Tests/periodic.py" "$BUILD/grid.ppm" bands)"
|
||||
[ "$BANDED" = "yes" ] \
|
||||
&& result ok "the attribute nibble recolours it" "each cell row is its own scheme" \
|
||||
|| result no "the attribute nibble recolours it" "$BANDED"
|
||||
else
|
||||
result no "a program drew a grid of its own tile" "no picture came out"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user