mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-11-01 12:14:56 +02:00
feat(badapple): FILL primitive and one-frame MAME check
This commit is contained in:
1 parent
beb45942ad
commit
dbfc7b60c5
9 files changed
+540
No files matched your search
@@ -2,5 +2,6 @@ data/*
|
||||
!data/manifest.json
|
||||
preview/
|
||||
player/build/
|
||||
player/work/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/bin/sh
|
||||
# Build the Bad Apple payload with the 1987 TI TMS34010 SDK under DOSBox.
|
||||
#
|
||||
# .text at bit 0x018C0000, .bss at bit 0x01C00000 (the free block the rdebug
|
||||
# loader already uses). Outputs: build/player.coff, player.bin, player.env.
|
||||
set -eu
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
WORK="$DIR/work"
|
||||
OUT="$DIR/build"
|
||||
IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img"
|
||||
DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf"
|
||||
|
||||
TEXT=018C0000
|
||||
BSS=01C00000
|
||||
|
||||
mkdir -p "$WORK" "$OUT"
|
||||
|
||||
if [ ! -x "$WORK/GSPCG.EXE" ]; then
|
||||
mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/"
|
||||
mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/"
|
||||
fi
|
||||
|
||||
cp "$DIR/main.c" "$WORK/MAIN.C"
|
||||
cp "$DIR/movie.c" "$WORK/MOVIE.C"
|
||||
cp "$DIR/movie.h" "$WORK/MOVIE.H"
|
||||
cp "$DIR/fill.asm" "$WORK/FILL.ASM"
|
||||
cp "$DIR/entry.asm" "$WORK/ENTRY.ASM"
|
||||
|
||||
{
|
||||
printf '%s\n' '-a'
|
||||
printf '%s\n' '-o PLAYER.out'
|
||||
printf '%s\n' '-m PLAYER.map'
|
||||
printf '%s\n' '-e _start'
|
||||
printf '%s\n' 'MAIN.obj'
|
||||
printf '%s\n' 'MOVIE.obj'
|
||||
printf '%s\n' 'FILL.obj'
|
||||
printf '%s\n' 'ENTRY.obj'
|
||||
printf '%s\n' '-l RTS.LIB'
|
||||
printf '%s\n' 'MEMORY'
|
||||
printf '%s\n' '{'
|
||||
printf '%s\n' " prog: origin = ${TEXT}h, length = 20000h"
|
||||
printf '%s\n' " ram: origin = ${BSS}h, length = 200000h"
|
||||
printf '%s\n' '}'
|
||||
printf '%s\n' 'SECTIONS'
|
||||
printf '%s\n' '{'
|
||||
printf '%s\n' ' .text: {} > prog'
|
||||
printf '%s\n' ' .data: {} > prog'
|
||||
printf '%s\n' ' .cinit: {} > prog'
|
||||
printf '%s\n' ' .bss: {} > ram'
|
||||
printf '%s\n' '}'
|
||||
} > "$WORK/PLAYER.CMD"
|
||||
sed -i 's/$/\r/' "$WORK/PLAYER.CMD"
|
||||
|
||||
{
|
||||
printf 'gspcpp MAIN\r\n'
|
||||
printf 'gspcc MAIN\r\n'
|
||||
printf 'gspcg MAIN\r\n'
|
||||
printf 'gspa MAIN MAIN.obj MAIN.lst -l\r\n'
|
||||
printf 'gspcpp MOVIE\r\n'
|
||||
printf 'gspcc MOVIE\r\n'
|
||||
printf 'gspcg MOVIE\r\n'
|
||||
printf 'gspa MOVIE MOVIE.obj MOVIE.lst -l\r\n'
|
||||
printf 'gspa FILL FILL.obj FILL.lst -l\r\n'
|
||||
printf 'gspa ENTRY ENTRY.obj ENTRY.lst -l\r\n'
|
||||
printf 'gsplnk PLAYER.cmd\r\n'
|
||||
printf 'exit\r\n'
|
||||
} > "$WORK/BUILD.BAT"
|
||||
|
||||
rm -f "$WORK"/MAIN.OBJ "$WORK"/MOVIE.OBJ "$WORK"/FILL.OBJ \
|
||||
"$WORK"/ENTRY.OBJ "$WORK"/PLAYER.OUT "$WORK"/PLAYER.MAP \
|
||||
"$WORK"/MAIN.LST "$WORK"/MOVIE.LST "$WORK"/FILL.LST \
|
||||
"$WORK"/ENTRY.LST
|
||||
|
||||
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
|
||||
timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \
|
||||
-c "mount c $WORK" -c "c:" \
|
||||
-c "BUILD.BAT" \
|
||||
-c "exit" -exit > "$WORK/dosbox.log" 2>&1
|
||||
|
||||
if [ ! -f "$WORK/PLAYER.OUT" ]; then
|
||||
echo "build.sh: GSPLNK produced no PLAYER.OUT" >&2
|
||||
sed 's/\r$//' "$WORK/PLAYER.MAP" 2>/dev/null | tail -20 >&2 || true
|
||||
for f in MAIN.LST MOVIE.LST FILL.LST ENTRY.LST; do
|
||||
sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' \
|
||||
| grep -vi 'no errors' >&2 || true
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
for f in MAIN.LST MOVIE.LST FILL.LST ENTRY.LST; do
|
||||
if sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' \
|
||||
| grep -qvi 'no errors'; then
|
||||
echo "build.sh: $f diagnostics:" >&2
|
||||
sed 's/\r$//' "$WORK/$f" | grep -in 'error' \
|
||||
| grep -vi 'no errors' >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cp "$WORK/PLAYER.OUT" "$OUT/player.coff"
|
||||
sed 's/\r$//' "$WORK/PLAYER.MAP" > "$OUT/player.map"
|
||||
sed 's/\r$//' "$WORK/MAIN.LST" > "$OUT/main.lst" 2>/dev/null || true
|
||||
sed 's/\r$//' "$WORK/MOVIE.LST" > "$OUT/movie.lst" 2>/dev/null || true
|
||||
sed 's/\r$//' "$WORK/FILL.LST" > "$OUT/fill.lst" 2>/dev/null || true
|
||||
|
||||
python3 "$ROOT/harness/hello-world/cofextract.py" \
|
||||
"$WORK/PLAYER.OUT" "$OUT/player.bin" > "$OUT/cofextract.txt"
|
||||
|
||||
ENTRY_PTR=$(sed 's/\r$//' "$WORK/PLAYER.MAP" \
|
||||
| awk '/ENTRY POINT SYMBOL/ { print $NF; exit }')
|
||||
if [ -z "$ENTRY_PTR" ]; then
|
||||
echo "build.sh: no entry point in PLAYER.map" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'ENTRY_PTR=0x%s\n' "$ENTRY_PTR" > "$OUT/player.env"
|
||||
|
||||
echo "build.sh: $OUT/player.bin ($(wc -c < "$OUT/player.bin") bytes)"
|
||||
grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/player.map" | head -8
|
||||
echo "build.sh: entry bit 0x$ENTRY_PTR"
|
||||
@@ -0,0 +1,18 @@
|
||||
; entry.asm -- payload entry for the Bad Apple player.
|
||||
;
|
||||
; The payload is entered by a debugger/rdebug jump and never returns. Set the
|
||||
; C stack, call main, and spin if main ever returns.
|
||||
|
||||
.text
|
||||
.globl _start
|
||||
.globl _main
|
||||
_start:
|
||||
setf 32, 0, 1
|
||||
movi _cstack, a14
|
||||
move a14, a13
|
||||
calla _main
|
||||
spin:
|
||||
jruc spin
|
||||
|
||||
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
|
||||
.end
|
||||
@@ -0,0 +1,56 @@
|
||||
; fill.asm -- one FILL L rectangle into the 1280x1024, 256-byte-stride VRAM.
|
||||
;
|
||||
; The C wrapper publishes:
|
||||
; fill_daddr linear bit address of the top-left pixel
|
||||
; fill_dydx (rows << 16) | columns, in screen pixels
|
||||
; fill_color 0x0000 = black, 0xFFFF = white
|
||||
;
|
||||
; FILL L destroys DADDR and DYDX, so the wrapper reloads them every call.
|
||||
; PSIZE, PMASK and CONTROL are programmed once by _fill_init_.
|
||||
|
||||
.ref _fill_daddr
|
||||
.ref _fill_dydx
|
||||
.ref _fill_color
|
||||
.text
|
||||
|
||||
.globl _fill_run_
|
||||
_fill_run_:
|
||||
setf 32, 0, 1
|
||||
move b2, -*sp, 1
|
||||
move b3, -*sp, 1
|
||||
move b7, -*sp, 1
|
||||
move b9, -*sp, 1
|
||||
|
||||
move @_fill_daddr, b2, 1
|
||||
movi 800h, b3 ; 2048 bits = 256-byte row pitch
|
||||
move @_fill_dydx, b7, 1
|
||||
move @_fill_color, b9, 1
|
||||
|
||||
; PSIZE is cheap to re-assert; if the resident OS left it at 2 bpp,
|
||||
; FILL would write twice as many pixels as asked for.
|
||||
movi 1, a1
|
||||
move a1, @0C0000150h, 0
|
||||
|
||||
fill l
|
||||
|
||||
move *sp+, b9, 1
|
||||
move *sp+, b7, 1
|
||||
move *sp+, b3, 1
|
||||
move *sp+, b2, 1
|
||||
rets
|
||||
|
||||
.globl _fill_init_
|
||||
_fill_init_:
|
||||
setf 32, 0, 1
|
||||
movi 1, a1
|
||||
move a1, @0C0000150h, 0 ; PSIZE = 1
|
||||
clr a1
|
||||
move a1, @0C0000160h, 0 ; PMASK = 0
|
||||
move a1, @0C0000170h, 0 ; upper half, forward compatibility
|
||||
move @0C00000B0h, a1, 0 ; CONTROL
|
||||
andi 0801Fh, a1 ; keep CD and RM/RR, raster op 0
|
||||
move a1, @0C00000B0h, 0
|
||||
move @0C00000B0h, a1, 0 ; read back: I/O writes are pipelined
|
||||
rets
|
||||
|
||||
.end
|
||||
@@ -0,0 +1,74 @@
|
||||
/* main.c -- Bad Apple payload, one-frame mode and (later) playback. */
|
||||
#include "movie.h"
|
||||
|
||||
#define MOVIE_BASE 0x00080000
|
||||
#define MOVIE_MAX 0x00308000
|
||||
#define VRAM_BASE 0x04000000
|
||||
#define ROW_BITS 2048
|
||||
#define MARK 0x01810000
|
||||
|
||||
extern void fill_run_();
|
||||
extern void fill_init_();
|
||||
|
||||
unsigned int fill_daddr;
|
||||
unsigned int fill_dydx;
|
||||
unsigned int fill_color;
|
||||
|
||||
void fill_rect(x, y, w, h, color)
|
||||
int x;
|
||||
int y;
|
||||
int w;
|
||||
int h;
|
||||
int color;
|
||||
{
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
fill_daddr = VRAM_BASE + (unsigned int)y * ROW_BITS + (unsigned int)x;
|
||||
fill_dydx = ((unsigned int)h << 16) | (unsigned int)w;
|
||||
/* The decoder's colour is the canvas value: 1 = black, 0 = white. */
|
||||
fill_color = color ? 0x0000 : 0xFFFF;
|
||||
fill_run_();
|
||||
}
|
||||
|
||||
static void paint(x, y, w, h, color, ctx)
|
||||
int x;
|
||||
int y;
|
||||
int w;
|
||||
int h;
|
||||
int color;
|
||||
void *ctx;
|
||||
{
|
||||
fill_rect(x, y, w, h, color);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
struct movie_header header;
|
||||
int rc;
|
||||
|
||||
fill_init_();
|
||||
|
||||
/* Start from white; the OS left its own screen behind. */
|
||||
fill_rect(0, 0, 1280, 1024, 0);
|
||||
|
||||
rc = movie_open((unsigned char *)MOVIE_BASE, MOVIE_MAX, &header);
|
||||
if (rc != 0) {
|
||||
*(unsigned int *)MARK = 0xBAD00001;
|
||||
for (;;) {
|
||||
}
|
||||
}
|
||||
|
||||
rc = movie_decode_frame((unsigned char *)MOVIE_BASE, MOVIE_MAX, 0,
|
||||
&header, paint, 0);
|
||||
if (rc != 0) {
|
||||
*(unsigned int *)MARK = 0xBAD00002;
|
||||
for (;;) {
|
||||
}
|
||||
}
|
||||
|
||||
*(unsigned int *)MARK = 0; /* FRAME */
|
||||
*(unsigned int *)(MARK + 0x10 * 8) = 1; /* STATUS: painted */
|
||||
for (;;) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare a MAME VRAM dump with the logical frame the movie should show."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from pipeline.movie import read_movie
|
||||
from pipeline.tilepack import apply_record
|
||||
|
||||
VRAM_STRIDE = 256
|
||||
|
||||
|
||||
def vram_to_logical(data: bytes, width: int = 640, height: int = 512,
|
||||
scale: int = 2) -> np.ndarray:
|
||||
"""Unpack a 1-bpp VRAM dump and majority-downsample scale×scale blocks."""
|
||||
screen_w = width * scale
|
||||
screen_h = height * scale
|
||||
need = VRAM_STRIDE * screen_h
|
||||
if len(data) < need:
|
||||
raise ValueError("VRAM dump is %d bytes, need at least %d"
|
||||
% (len(data), need))
|
||||
rows = np.frombuffer(data[:need], dtype=np.uint8).reshape(screen_h,
|
||||
VRAM_STRIDE)
|
||||
bits = np.unpackbits(rows, axis=1, bitorder="little")[:, :screen_w]
|
||||
blocks = bits.astype(np.uint16).reshape(height, scale, width, scale)
|
||||
# The driver's palette: bit 1 = white, bit 0 = black.
|
||||
return blocks.mean(axis=(1, 3)) < 0.5
|
||||
|
||||
|
||||
def expected_frame(movie_path: str, frame: int) -> np.ndarray:
|
||||
header, records = read_movie(movie_path)
|
||||
if frame < 0 or frame >= len(records):
|
||||
raise ValueError("frame %d outside 0..%d" % (frame, len(records) - 1))
|
||||
canvas = np.zeros((header.height, header.width), dtype=bool)
|
||||
for record in records[:frame + 1]:
|
||||
apply_record(canvas, record, tile=header.tile)
|
||||
return canvas
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--vram", required=True)
|
||||
parser.add_argument("--movie", required=True)
|
||||
parser.add_argument("--frame", type=int, default=0)
|
||||
parser.add_argument("--scale", type=int, default=2)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
data = Path(args.vram).read_bytes()
|
||||
header, _ = read_movie(args.movie)
|
||||
actual = vram_to_logical(data, header.width, header.height, args.scale)
|
||||
expected = expected_frame(args.movie, args.frame)
|
||||
if np.array_equal(actual, expected):
|
||||
print("EXACT frame %d" % args.frame)
|
||||
return 0
|
||||
bad = int(np.count_nonzero(actual != expected))
|
||||
ys, xs = np.where(actual != expected)
|
||||
print("MISMATCH frame %d: %d logical pixels differ" % (args.frame, bad))
|
||||
for y, x in list(zip(ys, xs))[:16]:
|
||||
print(" (%d,%d) expected=%d actual=%d"
|
||||
% (x, y, expected[y, x], actual[y, x]))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,151 @@
|
||||
-- load_mame.lua -- debugger entry for the Bad Apple payload.
|
||||
--
|
||||
-- Loads the payload at bit 0x018C0000 and the movie at bit 0x00080000, then
|
||||
-- enters the payload at the OS idle loop. This is the development path; the
|
||||
-- real delivery path is tools/load.sh (rdebug + LANCE).
|
||||
--
|
||||
-- Env:
|
||||
-- BA_MODE=one|play (one is the only mode implemented here)
|
||||
-- BA_MOVIE movie file (default bad-apple/data/test_movie.bin)
|
||||
-- BA_PAYLOAD raw payload (default bad-apple/player/build/player.bin)
|
||||
-- BA_ENV player.env with ENTRY_PTR (default .../player.env)
|
||||
-- BA_AT load frame (default 2500)
|
||||
-- BA_END inspection/exit frame (default 2600)
|
||||
|
||||
local function hex(v, w)
|
||||
return string.format("%0" .. (w or 8) .. "X", v)
|
||||
end
|
||||
|
||||
local machine = manager.machine
|
||||
local cpu = machine.devices[':maincpu']
|
||||
local dbg = machine.debugger
|
||||
local prog = cpu.spaces['program']
|
||||
|
||||
if not dbg then
|
||||
print("BA ERROR: debugger not enabled; pass -debug")
|
||||
machine:exit()
|
||||
return
|
||||
end
|
||||
|
||||
local ORIGIN = 0x018C0000
|
||||
local MOVIE = 0x00080000
|
||||
local MARK = 0x01810000
|
||||
local ENTRY_PC = 0x3F579970
|
||||
local AT = tonumber(os.getenv("BA_AT") or "2500")
|
||||
local STOP_AT = tonumber(os.getenv("BA_END") or "2600")
|
||||
local MOVIE_PATH = os.getenv("BA_MOVIE") or "bad-apple/data/test_movie.bin"
|
||||
local PAYLOAD_PATH = os.getenv("BA_PAYLOAD") or "bad-apple/player/build/player.bin"
|
||||
local ENV_PATH = os.getenv("BA_ENV") or "bad-apple/player/build/player.env"
|
||||
|
||||
local function read_all(path)
|
||||
local f, err = io.open(path, "rb")
|
||||
if not f then
|
||||
print("BA ERROR: cannot open " .. path .. ": " .. tostring(err))
|
||||
machine:exit()
|
||||
return nil
|
||||
end
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
return data
|
||||
end
|
||||
|
||||
local function entry_from_env()
|
||||
local explicit = os.getenv("BA_ENTRY")
|
||||
if explicit then
|
||||
return tonumber(explicit)
|
||||
end
|
||||
local f = io.open(ENV_PATH, "r")
|
||||
if not f then
|
||||
return ORIGIN
|
||||
end
|
||||
for line in f:lines() do
|
||||
local value = line:match("ENTRY_PTR=0x(%x+)")
|
||||
if value then
|
||||
f:close()
|
||||
return tonumber(value, 16)
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
return ORIGIN
|
||||
end
|
||||
|
||||
local payload = read_all(PAYLOAD_PATH)
|
||||
local movie = read_all(MOVIE_PATH)
|
||||
if not payload or not movie then
|
||||
return
|
||||
end
|
||||
local entry = entry_from_env()
|
||||
print(string.format("BA: payload %d bytes, movie %d bytes, entry %s",
|
||||
#payload, #movie, hex(entry)))
|
||||
|
||||
local function write_bytes(data, base)
|
||||
local i
|
||||
for i = 0, #data - 1 do
|
||||
prog:write_u8(base + i * 8, data:byte(i + 1))
|
||||
end
|
||||
end
|
||||
|
||||
local function verify(data, base)
|
||||
local i
|
||||
for i = 0, #data - 1 do
|
||||
if prog:read_u8(base + i * 8) ~= data:byte(i + 1) then
|
||||
return false, i
|
||||
end
|
||||
end
|
||||
return true, 0
|
||||
end
|
||||
|
||||
local loaded = false
|
||||
local entered = false
|
||||
local done = false
|
||||
local frames = 0
|
||||
|
||||
local function load_and_enter()
|
||||
write_bytes(payload, ORIGIN)
|
||||
write_bytes(movie, MOVIE)
|
||||
local ok, bad = verify(payload, ORIGIN)
|
||||
if not ok then
|
||||
print("BA ERROR: payload readback mismatch at byte " .. bad)
|
||||
machine:exit()
|
||||
return
|
||||
end
|
||||
ok, bad = verify(movie, MOVIE)
|
||||
if not ok then
|
||||
print("BA ERROR: movie readback mismatch at byte " .. bad)
|
||||
machine:exit()
|
||||
return
|
||||
end
|
||||
print("BA: loaded; breakpoint at " .. hex(ENTRY_PC))
|
||||
dbg:command("bpset " .. hex(ENTRY_PC) .. ",,{ pc = " .. hex(entry) .. " ; g }")
|
||||
end
|
||||
|
||||
emu.register_frame_done(function()
|
||||
frames = frames + 1
|
||||
if frames == AT and not loaded then
|
||||
loaded = true
|
||||
load_and_enter()
|
||||
end
|
||||
if loaded and not entered and prog:read_u16(MARK) == 0x0000
|
||||
and prog:read_u16(MARK + 0x10 * 8) == 0x0001 then
|
||||
entered = true
|
||||
done = true
|
||||
print(string.format("BA: STATUS=painted at frame %d; FRAME=%d",
|
||||
frames, prog:read_u16(MARK)))
|
||||
end
|
||||
if loaded and not done and frames > AT + 30 then
|
||||
local err = prog:read_u32(MARK)
|
||||
if err ~= 0 then
|
||||
print(string.format("BA ERROR: marker %s at frame %d",
|
||||
hex(err), frames))
|
||||
machine:exit()
|
||||
end
|
||||
end
|
||||
if frames >= STOP_AT then
|
||||
print(string.format("BA: end at frame %d (loaded=%s done=%s)",
|
||||
frames, tostring(loaded), tostring(done)))
|
||||
machine:exit()
|
||||
end
|
||||
end, "vt1200-badapple-loader")
|
||||
|
||||
dbg.visible_cpu = cpu
|
||||
dbg.execution_state = 'run'
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make a small known movie for the one-frame MAME FILL check."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from pipeline.movie import FrameRecord, MovieHeader, write_movie
|
||||
from pipeline.tilepack import diff_tiles
|
||||
|
||||
|
||||
def main() -> int:
|
||||
width, height = 640, 512
|
||||
canvas = np.zeros((height, width), dtype=bool)
|
||||
canvas[20:70, 10:110] = True # a rectangle crossing tiles
|
||||
canvas[96:128, 160:192] = True # a solid logical tile at (5,3)
|
||||
records = diff_tiles(np.zeros_like(canvas), canvas, tile=32)
|
||||
frames = [FrameRecord(1, 0, records)]
|
||||
header = MovieHeader(width, height, 2, 32, width // 32, height // 32,
|
||||
len(frames), 60, 72)
|
||||
out = ROOT / "data" / "test_movie.bin"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_movie(str(out), frames, header)
|
||||
print("%s: %d bytes, %d dirty tiles"
|
||||
% (out, out.stat().st_size, len(records)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,11 @@
|
||||
from tools.check_vram import vram_to_logical
|
||||
|
||||
|
||||
def test_vram_downsamples_2x2_lsb_first():
|
||||
# VRAM bit 1 is white, bit 0 is black (the driver's palette).
|
||||
vram = bytearray(256 * 1024)
|
||||
vram[0] = 0x0C # bits 2 and 3 white in row 0
|
||||
vram[256] = 0x0C # bits 2 and 3 white in row 1
|
||||
logical = vram_to_logical(bytes(vram), width=640, height=512, scale=2)
|
||||
assert logical[0, 0] # bits 0..1 are black
|
||||
assert not logical[0, 1] # bits 2..3 are white
|
||||
Reference in new issue
Block a user