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:
+114
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user