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)")
|
||||
@@ -0,0 +1,61 @@
|
||||
import numpy as np, glob
|
||||
from PIL import Image
|
||||
|
||||
files = sorted(glob.glob("an2/f*.png"))
|
||||
imgs = [Image.open(f) for f in files]
|
||||
print("PIL mode:", imgs[0].mode)
|
||||
idx = [np.asarray(im.convert("P") if im.mode!="P" else im, dtype=np.uint8) for im in imgs]
|
||||
N = len(idx); H,W = idx[0].shape; raw = H*W
|
||||
print(f"frames={N} size={H}x{W} raw={raw} B/frame")
|
||||
|
||||
# --- per-pair change stats on the QUANTIZED stream ---
|
||||
pct = np.array([100.0*np.count_nonzero(idx[i]!=idx[i-1])/raw for i in range(1,N)])
|
||||
print(f"\n--- consecutive change % (24fps, quantized) ---")
|
||||
print(f"mean {pct.mean():.1f} median {np.median(pct):.1f} p10 {np.percentile(pct,10):.1f} p90 {np.percentile(pct,90):.1f} max {pct.max():.1f}")
|
||||
print(f"pairs under 2% changed: {np.count_nonzero(pct<2.0)}/{len(pct)} ({100*np.count_nonzero(pct<2.0)/len(pct):.0f}%)")
|
||||
|
||||
# --- threshold dedupe (twos detection) ---
|
||||
THRESH = 2.0
|
||||
keep=[0]
|
||||
for i in range(1,N):
|
||||
if 100.0*np.count_nonzero(idx[i]!=idx[keep[-1]])/raw >= THRESH:
|
||||
keep.append(i)
|
||||
print(f"\n--- dedupe @ {THRESH}% ---")
|
||||
print(f"unique frames: {len(keep)}/{N} -> effective {len(keep)/12.0:.1f} fps")
|
||||
|
||||
cp = np.array([100.0*np.count_nonzero(idx[keep[j]]!=idx[keep[j-1]])/raw for j in range(1,len(keep))])
|
||||
print(f"unique-pair change %: mean {cp.mean():.1f} median {np.median(cp):.1f} p90 {np.percentile(cp,90):.1f} max {cp.max():.1f}")
|
||||
|
||||
def encode_size(a,b,gap=4):
|
||||
total=0
|
||||
for y in range(a.shape[0]):
|
||||
ra,rb=a[y],b[y]
|
||||
d=np.nonzero(ra!=rb)[0]
|
||||
if len(d)==0: continue
|
||||
spans=[]; s=d[0]; p=d[0]
|
||||
for x in d[1:]:
|
||||
if x-p>gap: spans.append((s,p)); s=x
|
||||
p=x
|
||||
spans.append((s,p))
|
||||
total+=2
|
||||
for s0,e0 in spans:
|
||||
seg=rb[s0:e0+1]; total+=2
|
||||
i2=0
|
||||
while i2<len(seg):
|
||||
r=1
|
||||
while i2+r<len(seg) and seg[i2+r]==seg[i2] and r<127: r+=1
|
||||
total += 2 if r>=3 else r
|
||||
i2+=r
|
||||
return total
|
||||
|
||||
sz=np.array([encode_size(idx[keep[j-1]],idx[keep[j]]) for j in range(1,len(keep))])
|
||||
fps_eff=len(keep)/12.0
|
||||
rate=sz.mean()*fps_eff
|
||||
print(f"\n--- codec estimate ---")
|
||||
print(f"delta bytes: mean {sz.mean():.0f} median {np.median(sz):.0f} p90 {np.percentile(sz,90):.0f} max {sz.max():.0f}")
|
||||
print(f"ratio vs raw: {raw/sz.mean():.1f}:1")
|
||||
print(f"stream rate : {rate/1024:.1f} KB/s")
|
||||
print(f"22min video : {rate*22*60/1048576:.0f} MB")
|
||||
px=cp.mean()/100*raw
|
||||
print(f"blit cost : ~{px*6.5/1000:.0f}k cycles/frame (budget 833k)")
|
||||
print(f"p90 blit : ~{np.percentile(cp,90)/100*raw*6.5/1000:.0f}k cycles")
|
||||
@@ -0,0 +1,11 @@
|
||||
import numpy as np, glob, sys
|
||||
from PIL import Image
|
||||
d=sys.argv[1]
|
||||
f=sorted(glob.glob(f"{d}/f*.png"))
|
||||
a=[np.asarray(Image.open(x).convert("RGB"),dtype=np.int16) for x in f]
|
||||
# noise-tolerant per-pair change: % of pixels differing by more than 8 levels
|
||||
p=np.array([100.0*np.count_nonzero(np.abs(a[i]-a[i-1]).max(axis=2)>8)/(a[0].shape[0]*a[0].shape[1]) for i in range(1,len(a))])
|
||||
print(f" pairs={len(p)} mean={p.mean():.2f}% median={np.median(p):.2f}% p90={np.percentile(p,90):.2f}% max={p.max():.2f}%")
|
||||
ev,od=p[0::2],p[1::2]
|
||||
print(f" even-idx pairs mean={ev.mean():.2f}% odd-idx pairs mean={od.mean():.2f}% ratio={max(ev.mean(),od.mean())/max(min(ev.mean(),od.mean()),1e-9):.1f}x")
|
||||
print(f" first 16 pairs: {np.round(p[:16],2)}")
|
||||
@@ -0,0 +1,54 @@
|
||||
import numpy as np, glob, os
|
||||
from PIL import Image
|
||||
|
||||
def enc(a,b,gap=4):
|
||||
t=0
|
||||
for y in range(a.shape[0]):
|
||||
ra,rb=a[y],b[y]; d=np.nonzero(ra!=rb)[0]
|
||||
if not len(d): continue
|
||||
sp=[]; s=d[0]; p=d[0]
|
||||
for x in d[1:]:
|
||||
if x-p>gap: sp.append((s,p)); s=x
|
||||
p=x
|
||||
sp.append((s,p)); t+=2
|
||||
for s0,e0 in sp:
|
||||
seg=rb[s0:e0+1]; t+=2; i=0
|
||||
while i<len(seg):
|
||||
r=1
|
||||
while i+r<len(seg) and seg[i+r]==seg[i] and r<127: r+=1
|
||||
t += 2 if r>=3 else r; i+=r
|
||||
return t
|
||||
|
||||
RAW=256*192; rows=[]
|
||||
for d in sorted(glob.glob("samp/*")):
|
||||
s=os.path.basename(d)
|
||||
fs=sorted(glob.glob(f"{d}/f*.png"))
|
||||
if len(fs)<10: continue
|
||||
rgb=[np.asarray(Image.open(x).convert("RGB")) for x in fs]
|
||||
# one shared palette per scene, no dither (cel art is flat)
|
||||
samp=np.concatenate([r.reshape(-1,3) for r in rgb[::3]])
|
||||
ref=Image.fromarray(samp.reshape(-1,1,3)).quantize(colors=256,method=Image.MEDIANCUT,dither=Image.NONE)
|
||||
idx=[np.asarray(Image.fromarray(r).quantize(palette=ref,dither=Image.NONE),dtype=np.uint8) for r in rgb]
|
||||
N=len(idx)
|
||||
# dedupe @0.3%
|
||||
keep=[0]
|
||||
for i in range(1,N):
|
||||
if 100.0*np.count_nonzero(idx[i]!=idx[keep[-1]])/RAW >= 0.3: keep.append(i)
|
||||
fps=len(keep)/5.0
|
||||
cp=np.array([100.0*np.count_nonzero(idx[keep[j]]!=idx[keep[j-1]])/RAW for j in range(1,len(keep))])
|
||||
sz=np.array([enc(idx[keep[j-1]],idx[keep[j]]) for j in range(1,len(keep))])
|
||||
rows.append((s,N,len(keep),fps,cp.mean(),np.percentile(cp,90),sz.mean(),np.percentile(sz,90),sz.mean()*fps))
|
||||
print(f"{s}: src={N} uniq={len(keep)} fps={fps:4.1f} chg={cp.mean():5.1f}%/p90 {np.percentile(cp,90):5.1f}% delta={sz.mean():6.0f}B/p90 {np.percentile(sz,90):6.0f} rate={sz.mean()*fps/1024:6.1f}KB/s")
|
||||
|
||||
r=np.array([x[3:] for x in rows],dtype=float)
|
||||
print("\n=== AGGREGATE (8 scenes) ===")
|
||||
print(f"effective fps after dedupe : {r[:,0].mean():.1f}")
|
||||
print(f"pixels changed / frame : mean {r[:,1].mean():.1f}% p90 {r[:,2].mean():.1f}%")
|
||||
print(f"delta frame size : mean {r[:,3].mean():.0f} B p90 {r[:,4].mean():.0f} B")
|
||||
print(f"compression vs raw : {RAW/r[:,3].mean():.1f}:1")
|
||||
rate=r[:,5].mean()
|
||||
print(f"stream rate : {rate/1024:.0f} KB/s")
|
||||
print(f"22 min of video : {rate*22*60/1048576:.0f} MB")
|
||||
px=r[:,1].mean()/100*RAW
|
||||
print(f"blit cost mean : ~{px*6.5/1000:.0f}k cycles (budget 833k @12fps)")
|
||||
print(f"blit cost p90 : ~{r[:,2].mean()/100*RAW*6.5/1000:.0f}k cycles")
|
||||
Reference in New Issue
Block a user