Session 1: hardware research, content measurement, codec decision, MAME harness
Verified GVRAM is one word-access per pixel in ALL color modes; chose 256-color 256x192 with movem.l bursts (page 1 sacrificed as double-buffer). Measured 8 scenes from the Blu-ray source: blit costs under 8% of the 12fps cycle budget, so I/O is the bottleneck, not CPU. Naive delta+RLE reaches only 3.2:1 (365 KB/s, 470MB) -> decision to use 4x4 vector quantization (~30 KB/s). "Shot on twos" assumption failed: the transfer has zero duplicate frames, so 12fps requires explicit decimation. Documents three false measurement results and their root causes (per-frame Floyd-Steinberg dithering, temporal denoise, exact-match dedupe on noisy source). MAME Lua injection harness works and is reusable for cycle-cost measurement; the IOCS _B_READ disk benchmark is blocked returning -1. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import numpy as np, glob
|
||||
from PIL import Image
|
||||
|
||||
files = sorted(glob.glob("an/f*.png"))
|
||||
rgb = [np.asarray(Image.open(f).convert("RGB")) for f in files]
|
||||
N = len(rgb)
|
||||
print(f"frames={N} size={rgb[0].shape}")
|
||||
|
||||
# --- 1. duplicate / twos detection on the SOURCE ---
|
||||
exact = 0; near = 0; diffs = []
|
||||
for i in range(1, N):
|
||||
d = np.abs(rgb[i].astype(np.int16) - rgb[i-1].astype(np.int16))
|
||||
m = d.mean()
|
||||
diffs.append(m)
|
||||
if m == 0: exact += 1
|
||||
elif m < 1.0: near += 1
|
||||
print(f"\n--- source frame-to-frame (24fps) ---")
|
||||
print(f"exact duplicates : {exact}/{N-1} ({100*exact/(N-1):.1f}%)")
|
||||
print(f"near-dup (<1.0) : {near}/{N-1} ({100*near/(N-1):.1f}%)")
|
||||
print(f"mean abs diff : {np.mean(diffs):.2f}")
|
||||
|
||||
# --- 2. build a per-scene 256-color palette from the whole clip ---
|
||||
sample = np.concatenate([rgb[i].reshape(-1,3) for i in range(0,N,4)])
|
||||
pal_img = Image.fromarray(sample.reshape(-1,1,3).astype(np.uint8))
|
||||
pal = pal_img.quantize(colors=256, method=Image.MEDIANCUT, dither=Image.NONE)
|
||||
palette = pal.getpalette()[:768]
|
||||
ref = Image.new("P", (1,1)); ref.putpalette(palette)
|
||||
|
||||
idx = []
|
||||
for a in rgb:
|
||||
q = Image.fromarray(a).quantize(palette=ref, dither=Image.FLOYDSTEINBERG)
|
||||
idx.append(np.asarray(q, dtype=np.uint8))
|
||||
|
||||
# quantization error
|
||||
err = np.mean([np.abs(np.asarray(Image.fromarray(idx[i]).convert("P")) ) for i in range(0)]) if False else None
|
||||
|
||||
# --- 3. drop duplicate frames -> unique frame stream ---
|
||||
keep = [0]
|
||||
for i in range(1, N):
|
||||
if not np.array_equal(idx[i], idx[keep[-1]]):
|
||||
keep.append(i)
|
||||
print(f"\n--- after 256-color quantize + dedupe ---")
|
||||
print(f"unique frames : {len(keep)}/{N} -> effective {len(keep)/12.0:.1f} fps")
|
||||
|
||||
# --- 4. delta sparsity between consecutive UNIQUE frames ---
|
||||
changed_pct = []
|
||||
for j in range(1, len(keep)):
|
||||
a, b = idx[keep[j-1]], idx[keep[j]]
|
||||
changed_pct.append(100.0*np.count_nonzero(a != b)/a.size)
|
||||
print(f"pixels changed : mean {np.mean(changed_pct):.1f}% median {np.median(changed_pct):.1f}% p90 {np.percentile(changed_pct,90):.1f}% max {np.max(changed_pct):.1f}%")
|
||||
|
||||
# --- 5. estimate compressed size: row-span delta + RLE within span ---
|
||||
def encode_size(a, b, gap=4):
|
||||
total = 0
|
||||
H, W = a.shape
|
||||
for y in range(H):
|
||||
ra, rb = a[y], b[y]
|
||||
diff = np.nonzero(ra != rb)[0]
|
||||
if len(diff) == 0: continue
|
||||
# merge runs separated by < gap
|
||||
spans = []; s = diff[0]; p = diff[0]
|
||||
for x in diff[1:]:
|
||||
if x - p > gap: spans.append((s,p)); s = x
|
||||
p = x
|
||||
spans.append((s,p))
|
||||
total += 2 # row header: y + span count
|
||||
for (s0,e0) in spans:
|
||||
seg = rb[s0:e0+1]
|
||||
total += 2 # x start + length
|
||||
# RLE within segment
|
||||
i2 = 0; cost = 0
|
||||
while i2 < len(seg):
|
||||
run = 1
|
||||
while i2+run < len(seg) and seg[i2+run] == seg[i2] and run < 127: run += 1
|
||||
cost += 2 if run >= 3 else run
|
||||
i2 += run
|
||||
total += cost
|
||||
return total
|
||||
|
||||
sizes = [encode_size(idx[keep[j-1]], idx[keep[j]]) for j in range(1, len(keep))]
|
||||
print(f"\n--- codec estimate (row-span delta + RLE) ---")
|
||||
print(f"delta frame bytes: mean {np.mean(sizes):.0f} median {np.median(sizes):.0f} p90 {np.percentile(sizes,90):.0f} max {np.max(sizes):.0f}")
|
||||
raw = 256*192
|
||||
print(f"vs raw {raw} B/frame -> mean ratio {raw/np.mean(sizes):.1f}:1")
|
||||
|
||||
fps_eff = len(keep)/12.0
|
||||
byterate = np.mean(sizes)*fps_eff
|
||||
print(f"\nstream rate : {byterate/1024:.1f} KB/s")
|
||||
print(f"22 min extrapol. : {byterate*22*60/1048576:.0f} MB video")
|
||||
# cycle cost: ~1 word write per changed pixel, movem amortized
|
||||
mean_changed_px = np.mean(changed_pct)/100*raw
|
||||
print(f"mean changed px : {mean_changed_px:.0f} -> blit ~{mean_changed_px*6.5/1000:.0f}k cycles (budget 833k)")
|
||||
Reference in New Issue
Block a user