#!/usr/bin/env bash # Checks what the video device actually draws. # # THE SUITE HAS NO DISPLAY, and a screen nothing can look at is a screen nothing checks. So # the device renders into a buffer that is a pure function of video memory, and the machine # can be asked to save it with --screen. Every check below runs a program, saves the picture # and reads pixels out of it - no window, no display server, and the same answer every time. # # Each check is a named claim about one behaviour rather than a comparison against a # recorded image. A recorded image would say "something changed" and leave which of the # palette, the tile, the attribute, the map or the scroll register broke to be found by # hand, which for a screen is the hardest kind of bug to see. # # Written by Anachronaut set -u ROOT="$(cd "$(dirname "$0")/.." && pwd)" BUILD="$ROOT/Tests/build/video" ASM="$ROOT/Assembler" EMU="$ROOT/SplitBit" for tool in "$ASM" "$EMU"; do [ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; } done rm -rf "$BUILD"; mkdir -p "$BUILD" PASS=0 FAIL=0 FAILED_NAMES=() GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m' [ -t 1 ] || { GREEN=""; RED=""; RESET=""; } result() { # result if [ "$1" = "ok" ]; then PASS=$((PASS + 1)); printf " [%sok %s] %-38s %s\n" "$GREEN" "$RESET" "$2" "$3" else FAIL=$((FAIL + 1)); FAILED_NAMES+=("$2") printf " [%sFAIL%s] %-38s %s\n" "$RED" "$RESET" "$2" "$3" fi } # ---- Writing to video memory from a program ---- # # Through the controller, because that is the only way to reach a device's bank: the CPU # never touches it directly. The Data port puts a byte at the destination and steps the # address on, which is what makes a poke six instructions instead of a loop. prologue() { cat <<'ASM' #Program start: INIA 0d3 OUTA 0xE3 INIA 0x30 OUTA 0xE2 INIA 0x03 OUTA 0xE8 ; Video memory becomes bank 3 ASM } poke() { # poke
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 )) } port() { # port printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF )) } show() { # show - sends a port's value to the console, so a test can read a register. # INA reads straight into A, so there is nothing to move first. printf ' INA 0x%02X\n OUTA 0x00\n' $(( $1 & 0xFF )) } epilogue() { printf ' HALT\n#Vectors\n Boot start\n' } # Assembles what is on standard input, runs it, and leaves the picture in $BUILD/.ppm. run() { local name="$1" cat > "$BUILD/$name.asm" "$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || { echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; } "$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1 } # One pixel out of a PPM, as "r,g,b". pixel() { python3 - "$BUILD/$1.ppm" "$2" "$3" <<'PY' import sys data = open(sys.argv[1], "rb").read() # P6, width height, maxval, then the bytes. The header is three whitespace-separated # fields after the magic, which is all this needs to know about the format. fields = data.split(b"\n", 3) width, height = (int(n) for n in fields[1].split()) body = fields[3] x, y = int(sys.argv[2]), int(sys.argv[3]) at = (y * width + x) * 3 print("%d,%d,%d" % tuple(body[at:at + 3])) PY } size() { head -c 20 "$BUILD/$1.ppm" | sed -n '2p' } echo "Checking what the video device draws." # ---- A tile lands where it is put ---- # # Palette entry 1 is red, tile 1 is 64 pixels of index 1, and two cells name it: the corner # and column 3 of row 2. A tile drawn one cell out is the commonest way a tile engine is # wrong, so the check is where it is AND where it is not. { prologue poke 0xC004 0xFF; poke 0xC005 0x00; poke 0xC006 0x00 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 epilogue } | run corner || exit 1 [ "$(pixel corner 0 0)" = "255,0,0" ] \ && result ok "a tile lands where it is put" "cell 0,0 is red" \ || result no "a tile lands where it is put" "got $(pixel corner 0 0)" [ "$(pixel corner 7 7)" = "255,0,0" ] \ && result ok "and fills its whole cell" "pixel 7,7 too" \ || result no "and fills its whole cell" "got $(pixel corner 7 7)" [ "$(pixel corner 8 0)" = "0,0,0" ] \ && result ok "and stops at the cell edge" "pixel 8,0 is not" \ || result no "and stops at the cell edge" "got $(pixel corner 8 0)" [ "$(pixel corner 24 16)" = "255,0,0" ] \ && result ok "row 2 column 3 is where it says" "pixel 24,16" \ || result no "row 2 column 3 is where it says" "got $(pixel corner 24 16)" # ---- The palette is what colours it ---- # # Same tile, same map, a different palette entry. Nothing about the picture changes except # the three bytes the colour came from. { prologue poke 0xC004 0x00; poke 0xC005 0xFF; poke 0xC006 0x40 for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01; poke 0x4001 0x00 epilogue } | run palette || exit 1 [ "$(pixel palette 0 0)" = "0,255,64" ] \ && result ok "the palette is what colours it" "entry 1 moved, the tile did not" \ || result no "the palette is what colours it" "got $(pixel palette 0 0)" # ---- The attribute picks a palette bank ---- # # The tile is drawn in index 1 and never changes. Entry 1 is red and entry 17 is blue, and # the only difference between the two cells is the attribute nibble: 0 leaves the index # alone, 1 adds sixteen. This is the whole of the recolouring feature in one check. { prologue poke 0xC004 0xFF; poke 0xC005 0x00; poke 0xC006 0x00 poke 0xC044 0x00; poke 0xC045 0x00; poke 0xC046 0xFF for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01; poke 0x4001 0x00 poke 0x4002 0x01; poke 0x4003 0x01 epilogue } | run attribute || exit 1 [ "$(pixel attribute 0 0)" = "255,0,0" ] \ && result ok "attribute 0 leaves the index alone" "still entry 1" \ || result no "attribute 0 leaves the index alone" "got $(pixel attribute 0 0)" [ "$(pixel attribute 8 0)" = "0,0,255" ] \ && result ok "and attribute 1 adds sixteen" "the same tile, entry 17" \ || result no "and attribute 1 adds sixteen" "got $(pixel attribute 8 0)" # ---- Scrolling moves a register, not memory ---- # # The tile is in map row 3 and nothing moves it. Setting the scroll origin to 3 brings that # row to the top of the screen, which is the whole reason a terminal on this machine is # affordable at all. { prologue poke 0xC004 0xFF; poke 0xC005 0xFF; poke 0xC006 0x00 for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke $((0x4000 + 3 * 256)) 0x01 port 0x34 0x03 epilogue } | run scroll || exit 1 [ "$(pixel scroll 0 0)" = "255,255,0" ] \ && result ok "scrolling moves which row is on top" "map row 3 at screen row 0" \ || result no "scrolling moves which row is on top" "got $(pixel scroll 0 0)" [ "$(pixel scroll 0 8)" = "0,0,0" ] \ && result ok "and takes the rest with it" "map row 4 below it" \ || result no "and takes the rest with it" "got $(pixel scroll 0 8)" # ---- The map is a ring ---- # # Origin 127 with 128 rows puts map row 127 at the top and map row 0 immediately under it. # A map that clipped instead of wrapping would show nothing on the second row. { prologue poke 0xC004 0xFF; poke 0xC005 0xFF; poke 0xC006 0xFF for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done poke 0x4000 0x01 port 0x34 0x7F epilogue } | run ring || exit 1 [ "$(pixel ring 0 8)" = "255,255,255" ] \ && result ok "the map is a ring" "row 0 follows row 127" \ || result no "the map is a ring" "got $(pixel ring 0 8)" # ---- Modes ---- { prologue; port 0x31 0x01; epilogue; } | run wide || exit 1 [ "$(size wide)" = "640 400" ] \ && result ok "mode 1 is 640 by 400" "$(size wide)" \ || result no "mode 1 is 640 by 400" "got $(size wide)" { prologue; epilogue; } | run narrow || exit 1 [ "$(size narrow)" = "320 200" ] \ && result ok "and mode 0 is 320 by 200" "$(size narrow)" \ || result no "and mode 0 is 320 by 200" "got $(size narrow)" # The geometry is asked for rather than assumed, so a program can be written once and find # out what it is running on. { prologue; show 0x32; show 0x33; epilogue; } | run geometry || exit 1 GOT="$(head -c 2 "$BUILD/geometry.out" | od -An -tu1 | tr -s ' ' | sed 's/^ //;s/ $//')" [ "$GOT" = "40 25" ] \ && result ok "the ports say how big the screen is" "40 columns, 25 rows" \ || result no "the ports say how big the screen is" "got \"$GOT\"" # ---- A mode that does not exist ---- # # Not taken, and not fatal either. A screen is a poor place to stop the machine: a program # that asked for something impossible still has the screen it had. { prologue; port 0x31 0x09; show 0x32; epilogue; } | run badmode || exit 1 GOT="$(head -c 1 "$BUILD/badmode.out" | od -An -tu1 | tr -d ' ')" [ "$GOT" = "40" ] \ && result ok "an impossible mode is not taken" "still 40 columns" \ || result no "an impossible mode is not taken" "got $GOT" echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS video checks passed." exit 0 fi echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}" exit 1