#!/usr/bin/env python3
"""retroDE_ps2 — Ch356: composition == isolated RTL bit-for-bit check.

Renders each epoch ALONE (tb +ONLY=<k> +FBDUMP) over a precleared-black FB, then composites the N isolation dumps in
paint order (epoch 0, 1, ..., N-1; DECAL/overwrite) and asserts the result equals the joint ALL-mode render BIT-FOR-BIT.
This is Codex's Ch356 accumulation-correctness proof: the scheduler drawing the epochs together must produce exactly the
same framebuffer as compositing the individually-rendered epochs (no cache bleed, no stale pixels, correct rebind).

Composition operator (matches the RTL: precleared black bg, each epoch overwrites where it draws):
  compose[px] = the LAST epoch (highest k) that DREW px (isolation dump non-zero); else black.
Note: an epoch is DECAL/opaque, so its written value replaces whatever is underneath. A genuine opaque-BLACK texel
(dump==0 where the epoch drew) is indistinguishable from unwritten — but the joint ALL render is likewise black there
(black-over-anything = black in this content), so nonzero-wins composition still matches ALL bit-for-bit. Do NOT use
reference coverage to pick the owner: the RTL edge coverage differs slightly from the float reference, and a
reference-covered-but-RTL-unwritten pixel must fall through to the epoch that ACTUALLY drew it.

Usage: compose_sched.py <fb_ALL.hex> <fb_0.hex> <fb_1.hex> ...
Each *.hex file = W*H lines of 8-hex-digit PSMCT32 words (row-major), as emitted by +FBDUMP.
"""
import sys, os

def load(fn):
    out=[]
    with open(fn) as f:
        for ln in f:
            s=ln.strip()
            if not s or s.startswith("//"): continue
            out.append(int(s,16)&0xFFFFFFFF)
    return out

def main(argv):
    a=argv[1:]
    W=384
    if "--width" in a: i=a.index("--width"); W=int(a[i+1]); del a[i:i+2]
    if len(a)<3: sys.exit("usage: compose_sched.py <fb_ALL.hex> <fb_0.hex> <fb_1.hex> ...")
    all_fb=load(a[0]); eps=[load(x) for x in a[1:]]; N=len(eps)
    npx=len(all_fb)
    for k,e in enumerate(eps):
        if len(e)!=npx: sys.exit(f"[compose] epoch {k} dump {len(e)} px != ALL {npx}")

    comp=[0]*npx
    for px in range(npx):
        for k in range(N-1,-1,-1):
            if eps[k][px]!=0: comp[px]=eps[k][px]; break     # last epoch that actually DREW (DECAL nonzero-wins)

    mism=[px for px in range(npx) if comp[px]!=all_fb[px]]
    black_all=sum(1 for px in mism if all_fb[px]==0)
    black_cmp=sum(1 for px in mism if comp[px]==0)
    print(f"[compose] {N} epochs, {npx} px. composition vs ALL: {npx-len(mism)} exact, {len(mism)} mismatch "
          f"({100.0*(npx-len(mism))/npx:.4f}% exact)")
    if mism:
        print(f"[compose]   of the {len(mism)} mismatches: ALL==black:{black_all}  compose==black:{black_cmp}")
        for px in mism[:12]:
            print(f"[compose]   px {px} (x={px%W},y={px//W}): compose={comp[px]:08x} ALL={all_fb[px]:08x}")
    if not mism:
        print("[compose] PASS: joint ALL render == composited isolation dumps BIT-FOR-BIT (accumulation exact)")
        return 0
    print("[compose] FAIL: composition differs from ALL")
    return 1

if __name__=="__main__":
    raise SystemExit(main(sys.argv))
