qemu: review pass over the rig driver and boot script

Four review passes with fixes between them (flare-edge's flow-framework
review, 2026-09-09). qmp.py: drive() split out of a 330-line dispatcher,
every verb guarded so a raising verb records a fatal row instead of ending
the run, the shot path sanitised, the rs485 and wait verbs judged through
shared helpers; imgtools.py: a bench subcommand for phash/structural
timings and a colour probe that samples instead of scanning the frame;
ui-drive.sh: the boot poll no longer walks every pixel per tick and the
simulator's control socket path is passed as one word. Offline tests: 8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
Noah
2026-09-09 13:52:47 -06:00
co-authored by Claude Fable 5.1
parent 48be35dc7b
commit 8805b6106c
4 changed files with 1037 additions and 342 deletions
+114 -12
View File
@@ -8,6 +8,7 @@ plus a JSON reference. Pure Python plus Pillow only.
imgtools.py compare REFS NAME SHOT.ppm
imgtools.py capture REFS NAME SHOT.ppm X Y W H TOL # TOL: exact|loose|structural
imgtools.py hash SHOT.ppm
imgtools.py bench [SHOT.ppm] [N] # per-call phash/structural timing, N iterations
imgtools.py selftest
`compare` and `capture` read/write a reference file: JSON mapping a region
@@ -130,24 +131,35 @@ def _dct_basis(n):
_DCT32 = _dct_basis(32)
# phash() only ever reads the low-frequency _DCT_KEEP x _DCT_KEEP corner of
# the 32x32 DCT (see its docstring), so _dct2d_32 stops both passes at this
# many frequencies instead of computing all 1024 coefficients -- same 64
# coefficients out, roughly 6x less pure-Python multiply-add work per call.
_DCT_KEEP = 8
def _dct2d_32(rows):
"""2D DCT-II of a 32x32 list-of-lists via two separable 1D passes."""
"""2D DCT-II of a 32x32 list-of-lists via two separable 1D passes,
truncated to the low-frequency _DCT_KEEP x _DCT_KEEP corner -- the only
coefficients any caller reads."""
n = 32
# columns first: tmp[u][x] = DCT of column x at frequency u
tmp = [[0.0] * n for _ in range(n)]
for u in range(n):
# columns first: tmp[u][x] = DCT of column x at frequency u. Frequencies
# u >= _DCT_KEEP never feed a returned coefficient, so skip them.
tmp = [[0.0] * n for _ in range(_DCT_KEEP)]
for u in range(_DCT_KEEP):
b = _DCT32[u]
for x in range(n):
s = 0.0
for y in range(n):
s += rows[y][x] * b[y]
tmp[u][x] = s
# then rows: out[u][v] = DCT of tmp's row u at frequency v
out = [[0.0] * n for _ in range(n)]
for u in range(n):
# then rows: out[u][v] = DCT of tmp's row u at frequency v. Same cutoff
# on v; the y/x summations stay full since each kept coefficient still
# needs the whole 32-wide signal.
out = [[0.0] * _DCT_KEEP for _ in range(_DCT_KEEP)]
for u in range(_DCT_KEEP):
row = tmp[u]
for v in range(n):
for v in range(_DCT_KEEP):
b = _DCT32[v]
s = 0.0
for x in range(n):
@@ -166,7 +178,7 @@ def phash(img):
px = small.load()
rows = [[px[x, y] for x in range(32)] for y in range(32)]
coeffs = _dct2d_32(rows)
block = [coeffs[u][v] for u in range(8) for v in range(8)]
block = [coeffs[u][v] for u in range(_DCT_KEEP) for v in range(_DCT_KEEP)]
mean = sum(block[1:]) / (len(block) - 1)
bits = 0
for c in block:
@@ -350,13 +362,64 @@ def cmd_hash(argv):
return 0
def cmd_bench(argv):
"""Time phash()/structural() -- the per-step cost of every
assert_region/wait_region/capture_region in a flow run (wait_region polls
at 0.5s intervals, calling back into these on every poll), so a change to
the DCT size, the downscale filter, or the occupancy thresholds shows up
as a number here instead of only as a slower flow run nobody investigates.
usage: imgtools.py bench [SHOT.ppm] [N]
SHOT.ppm: a real screendump to benchmark against a representative crop
of; default is a synthetic 720x720 image (the virt.fragment screendump
size) so bench never depends on a committed fixture or a live rig.
N: iterations per function, default 200.
"""
import timeit
if len(argv) > 2:
print("usage: imgtools.py bench [SHOT.ppm] [N]", file=sys.stderr)
return 2
shot_path = next((a for a in argv if not a.isdigit()), None)
n = int(next((a for a in argv if a.isdigit()), "200"))
if shot_path:
w, h, data = load_ppm(shot_path)
else:
w, h = 720, 720
data = _make_test_rgb(w, h, (220, 20, 20))
img = (w, h, data)
# a representative crop, not the whole screen: every real region assert
# crops first, and structural()'s edge filter cost scales with crop
# size even though phash's fixed 32x32 downscale mostly doesn't.
cw, ch = min(200, w), min(200, h)
region = crop(img, 0, 0, cw, ch)
def percentiles(samples):
s = sorted(samples)
p50 = s[min(len(s) - 1, int(len(s) * 0.50))]
p95 = s[min(len(s) - 1, int(len(s) * 0.95))]
return s[0], p50, p95, s[-1]
print(f"bench: {cw}x{ch} region, n={n} iterations")
for label, fn in (("phash", lambda: phash(region)),
("structural", lambda: structural(region))):
samples = timeit.repeat(fn, repeat=n, number=1)
lo, p50, p95, hi = percentiles(samples)
print(f" {label:<10} min={lo * 1000:7.3f}ms p50={p50 * 1000:7.3f}ms "
f"p95={p95 * 1000:7.3f}ms max={hi * 1000:7.3f}ms")
return 0
# ---------------------------------------------------------------------------
# selftest
# ---------------------------------------------------------------------------
def _make_test_rgb(w, h, colour, shift=(0, 0)):
"""Black canvas with a coloured square near one corner, used only by
the self-test. colour=None means no square at all (removed shape)."""
"""Black canvas with a coloured square near one corner: the self-test's
synthetic images, and bench's default when it isn't given a real
SHOT.ppm. colour=None means no square at all (removed shape)."""
from PIL import ImageDraw
img = Image.new("RGB", (w, h), (0, 0, 0))
if colour is not None:
@@ -373,6 +436,33 @@ def _write_ppm(path, w, h, rgb_bytes):
f.write(rgb_bytes)
def _dct2d_32_brute(rows):
"""Unoptimised reference DCT: the full 32x32 transform with no early
cutoff. selftest()-only, so _dct2d_32's _DCT_KEEP truncation has
something independent to be checked against -- a future edit that moves
the cutoff on the wrong loop would otherwise change captured phash bits
with nothing catching it."""
n = 32
tmp = [[0.0] * n for _ in range(n)]
for u in range(n):
b = _DCT32[u]
for x in range(n):
s = 0.0
for y in range(n):
s += rows[y][x] * b[y]
tmp[u][x] = s
out = [[0.0] * n for _ in range(n)]
for u in range(n):
row = tmp[u]
for v in range(n):
b = _DCT32[v]
s = 0.0
for x in range(n):
s += row[x] * b[x]
out[u][v] = s
return out
def selftest():
import tempfile
@@ -403,6 +493,16 @@ def selftest():
check("crop keeps the drawn pixel in place",
cropped[2][px_off:px_off + 3] == bytes((220, 20, 20)))
# _dct2d_32's _DCT_KEEP truncation must land on the exact same
# coefficients an untruncated 32x32 DCT would produce
small = _to_pil((bw, bh, bdata)).convert("L").resize((32, 32), Image.LANCZOS)
px = small.load()
dct_rows = [[px[x, y] for x in range(32)] for y in range(32)]
fast, brute = _dct2d_32(dct_rows), _dct2d_32_brute(dct_rows)
check("_dct2d_32's truncated corner matches the untruncated DCT",
all(fast[u][v] == brute[u][v]
for u in range(_DCT_KEEP) for v in range(_DCT_KEEP)))
# identical shot: exact must pass
same_path = os.path.join(tmp, "same.ppm")
_write_ppm(same_path, w, h, _make_test_rgb(w, h, (220, 20, 20)))
@@ -434,6 +534,8 @@ def selftest():
check("compare on an unknown reference name fails loudly", rc == 1)
rc = main(["hash", base_path])
check("hash on a real ppm succeeds", rc == 0)
rc = main(["bench", base_path, "3"])
check("bench runs to completion", rc == 0)
if failures:
print(f"\n{len(failures)} check(s) failed:")
@@ -453,7 +555,7 @@ def main(argv):
cmd, rest = argv[0], argv[1:]
if cmd == "selftest":
return selftest()
handlers = {"compare": cmd_compare, "capture": cmd_capture, "hash": cmd_hash}
handlers = {"compare": cmd_compare, "capture": cmd_capture, "hash": cmd_hash, "bench": cmd_bench}
if cmd not in handlers:
print(f"unknown command: {cmd!r}", file=sys.stderr)
return 2