09a5a5006548050cf2ea09d33f56aa2e5b58eba4
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
09a5a50065 |
Measure the blit on the 68000: the 38% estimate was 53.6%
First 68000 instructions in this project to draw a pixel. Everything before this was GVRAM filled from Lua, which costs zero 68000 cycles, so the blit figure the whole CPU budget rests on had never been validated. Four variants of a full-frame 256x192 paint, timed in MAME and each also hand-derived from the MC68000 timing tables beforehand; the two agree to 0.006-0.43%, which is what makes the result trustworthy after this project's history of false-good measurements. V1 movem.l blit from a word-expanded RAM frame 446,286 cyc 53.6% V2 naive move.b/move.w per pixel 1,284,174 cyc 154.1% V3 write-only floor, no source read 225,789 cyc 27.1% V4 same writes in 4x4 block order 637,971 cyc 76.6% Scope: MAME's gvram_w/gvram_r carry no timing at all, so these are instruction cycles against zero-wait-state memory -- a floor, not a hardware prediction. V1's output snapshots pixel-exact through verify_frame256.py, closing FINDINGS 23.5. The V1/V3 gap shows reading the source frame is exactly half the cost, which makes the architecture question live: decode-direct-to-GVRAM needs no RAM reference frame and scales with the non-SKIP block fraction, crossing compose-then-blit at 70% of blocks changed. That fraction is now the top priority and is already a by-product of vq_hybrid.py's mode decision. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
7ba979a236 |
Handoff: green-light script, reconciled figures, a smaller first step for the decoder
Prepares session 4 for handoff. No new measurements; this reconciles the docs with what session 4 changed and makes the next session's entry point cheaper. - tools/bench/check.sh re-runs both display regression tests from the Blu-ray in ~40 s and prints ALL GREEN. Verified green cold, after wiping tmp/ and re-extracting. STATUS and README both open with it, because everything downstream assumes the display path is pixel-exact and nothing previously checked that in one command. - Reconciled the figures session 4 invalidated. Session 3's 38.88 dB ceiling is struck through in STATUS with a pointer to 40.81; the "three facts the player must honour" table no longer quotes R20 = 0x0116, which was the 768-wide IPL timing and would have been copied into the player as if it were the shipping value. crtc_mode.lua is now named as the single source of truth for CRTC registers, in both STATUS and README. The 38.88 dB in the session-3 reproduce section is left alone and annotated instead: it is correct for that test, which still packs I = 1. The two numbers disagree for a reason and a reader should be able to see which is which. - Next-step 2 now leads with something smaller than "write the decoder": a dumb full-frame RAM->GVRAM blit in 68000 code, timed. That number alone confirms or kills the 38% estimate, and needs no bitstream, codebooks, or DLX1 parsing. The reference image and its checker already exist. - Parked the user's Cliff Hanger / Lupin III follow-on in STATUS so it is not lost and not mistaken for scheduled work. Cheaper than this project on every axis except media prep, which is where it would actually stall. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
34f9ee341d |
A real 256x256 CRTC mode, derived not recalled; palette ceiling was 2 dB low
Session 3 left the harness on the IPL's 768x512 text timing because no CRTC values had been derived and guessing them was the failure mode to avoid. This derives them from MAME 0.277's divisor ladder instead, and the derivation is self-checking: the 256-wide mode runs at div 6 against the 768 mode's div 2, so htotal is exactly 1104/3 = 368 dots and every horizontal register divides by three with no remainder. Only the blanking split rounds. Verified by snapshot: native 256x512, active area pixel-exact, x=512 wrap gone. Two things fell out that change numbers elsewhere: - The palette's shared LSB I must be chosen per entry, not hardcoded to 1. Doing so lifts the display ceiling from 38.85 to 40.81 dB and is the only way to reach true black at all, since pal6bit(1) = 4. 102 of 256 entries want I = 0, so this is not a corner case. Supersedes FINDINGS 22.4; scsi has ~2 dB more headroom than that section claimed. The encoder does not do this yet. - Letterboxing costs a palette entry: GVRAM cleared to zero shows entry 0, and a free mediancut palette puts a real image colour there. 255 colours plus a reserved black, via prep_frame.py --reserve-black. MAME's graphics double-scan is phase-shifted one raster line (it halves the absolute scanline and vbegin is odd), which produced a false failure before it was understood; the regression test now asserts the shifted pairing explicitly. Still Lua-side. No 68000 instruction has drawn a pixel; the 38% blit estimate remains unvalidated. What this buys is a defined geometry for the decoder to write into: 256 words per row, 1024-byte stride, rows 32..223. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
966417893b |
Scope the display-path result: no 68000 code drew that frame
The session-3 milestone was written in a way that reads as "the port renders", which it does not. The video hardware is genuinely emulated and the output is bit-exact, but GVRAM was filled by a MAME Lua script poking emulated memory, not by 68000 instructions. The distinction is load-bearing: Lua writes cost zero 68000 cycles, so nothing here tests whether the CPU can decode and blit inside 833,333 cycles. The 38% full-frame blit estimate that the entire budget rests on is still unvalidated. Only the "Not yet started" list carried this caveat, which was too buried for a claim this easy to over-read. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
3265bf2740 |
Handoff: reconcile docs with the verified display path
Session 3 summary in STATUS.md, plus the things a cold start needs. - Reproduce section for the display result, verified cold from the Blu-ray at end of session: extract -> prep -> MAME -> verify, exact match, 38.88 dB. The frames are not in the repo and the old ones lived in /tmp, so the chain starts from extract.py rather than assuming a scratch directory survives. - tools/bench/verify_frame.py turns FINDINGS 22 into a regression check. It is deliberately an exact test rather than a PSNR threshold, since the whole point of that section is that the render is bit-for-bit predictable. It prints the three registers to check when it fails. - Recorded where the MAME source now lives, and why to read it first: six register-poking attempts failed against a gate that one grep found. - Split the CRTC mode table out as its own next step. It is the prerequisite for the decoder skeleton and the smallest well-defined task available, with an explicit warning not to write the timing values from memory. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
b322e84cd4 |
Get a real Dragon's Lair frame onto the emulated X68000
First pixels on an actual X68000 screen. Everything up to now was Python-side or a headless -video none run, which cannot snapshot at all. The blocker was not the video controller. The IPL leaves CRTC R20 = 0x0B16, and bit 11 is "G-VRAM set to buffer", which makes MAME's draw_gfx() return early. GVRAM writes still land and read back correctly while the layer is invisible, so six attempts at $E82400/$E82500/$E82600 all rendered black with every register holding the value I intended. Two more facts, both confirmed against MAME 0.277 source rather than assumed: - $E8E001 monitor contrast is left at 14 by the IPL, scaling all output to 93.3%. The player must set it to 15. Contrast 0 blanks the screen, which is a free fade-to-black for scene transitions. - The palette word is GGGGGRRRRRBBBBBI with a shared LSB, expanded as pal6bit((field<<1)|I). With contrast at 15 the render is pixel-exact, not merely close, which also confirms the 1024-byte GVRAM line stride. That exactness gives a new quality ceiling: the 15-bit+I palette alone costs 38.88 dB against the 24-bit palettised source, the same order as the scsi profile's own codec error. scsi is close to display-transparent on hardware, which bounds how much further it is worth raising. Unblocks next step 2, the 68000 decoder skeleton, which now has a known-good reference image to diff against. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
22c67f1cb8 |
Record the two shell traps that wedged session 2's background jobs
Eleven watcher shells and one MAME instance were left running for over an hour. Both had the same shape: a wait that can never be satisfied. - `until ! pgrep -f foo.py` matches the watching shell's own command line, so the loop never terminates. Wait on a PID or a sentinel file instead. - `timeout N mame` sends a SIGTERM that MAME ignores when its autoboot script is blocked; without `-k` the process spins at 100% CPU forever. Also gitignore vasm's default `a.out` output. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
64cd1ffd72 |
Handoff: reconcile docs and tooling with the corrections made this session
Session 2 reversed several of its own conclusions. The docs are append-only, so a reader could land on a superseded section and act on it. This pass makes the repo internally consistent. Defects found and fixed in STATUS.md: - claimed "Hybrid VQ with k=1024: no" as the answer to the linework question, directly contradicting FINDINGS 14, which rejected k=1024. Both profiles are k=256. - malformed profile table (six column separators, five columns). - next-steps list had two items numbered 3 and listed the full-disc survey twice. - the disk-benchmark section still read CRITICAL-PATH with "if SCSI sustains >=800 KB/s, ship pixel-exact". That was written while the bandwidth figure was misread as 4 MB/s. At 4 Mbps pixel-exact needs 92-97% of the pipe and is not available, and the ring-buffer result means the design no longer hangs on the benchmark at all. Rewritten with what it IS still worth doing: confirming the 4 Mbps provenance, and confirming DMA is used rather than PIO. FINDINGS now carries supersession blockquotes on 5, 8, 11, 17 and 18 pointing at the sections that correct them. 18 is the dangerous one -- its peak-vs- sustained test is reversed by 21 -- so it is marked DO NOT ACT ON THIS SECTION while noting the per-frame data itself remains valid. profile_gen.py had the same problem in code: it defaulted to the superseded peak sizing and returned lam=25 where the docs say lam=10. The buffered test is now the default and peak sizing is behind --size-for-peak as a bound only. A tool that contradicts the findings is worse than no tool. Also preserves the five measurement scripts that produced this session's numbers as tools/analysis/05-09, following the session 1 precedent, and adds an "explicitly abandoned -- do not re-propose" list to STATUS covering entropy coding, k=1024 codebooks and flat 4x4 VQ. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
fb8a1462b0 |
Correct the peak analysis: both profiles fit 4 Mbps with zero prefill
The previous commit warned that scsi "does not fit 4 Mbps" because a frame peaked at 96.4% of the sustained rate. That was the wrong test, and the user was right to push back on it. It compared instantaneous frame demand against a sustained rate as if they had to match frame-by-frame; the disk keeps filling during the frame, and a ring buffer absorbs any shortfall. Correct test is cumulative demand vs cumulative supply. Simulated on the real per-frame sizes at a constant 488 KB/s fill: required prefill = 0.0 KB for EVERY scene at both profiles Fill delivers 40.69 KB per frame time; only one measured frame (00146, 42.10 KB) exceeds it and the next frame recovers it. A 256 KB buffer -- 12.5% of RAM -- carries ~1 second of stall tolerance, orders of magnitude more than an SD-backed seek needs. scsi at lam=10 stands; the hardest sampled scene runs 313 KB/s mean, 64% of the pipe. Also carries through a consequence of SD-backed deployment that session 1 noted as a caveat but never applied: with BlueSCSI/SCSI2SD the sustained rate is a bus-limited CONSTANT, not an average over variable seek latency, so there is no long tail to leave margin for and we can size much closer to the ceiling. Conservative margins here were cargo-culted from a constraint this deployment does not have. The SASI/SCSI split remains correct because it is about bus protocol, not media. Rate control drops from load-bearing to insurance: intra-scene peaks are a non-problem, but only 4 clips of 1.2-1.7s out of 224 streams have been measured and 00146 already runs 23% hotter than 00020. The full-disc survey is now the highest-value measurement, ahead of wiring up ratectl. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |
||
|
|
48e912de8b |
Size against 4 Mbps: peaks break the scsi profile; DMA steal is not free
User clarified the bandwidth figure is 4 Mbps (488 KB/s), not 4 MB/s -- ~8x
tighter than the previous commit reasoned against. Two consequences, plus a
correction to session 1.
1. The scsi profile committed in
|
||
|
|
f0f2f807a4 |
Raise both quality profiles; rule out entropy coding on CPU grounds
The profiles shipped in
|
||
|
|
e4062ed294 |
Session 2: hybrid VQ codec, two quality profiles, three corrections
Answers session 1's critical-path question. Flat 4x4 VQ at k=256 was prototyped and REJECTED by eye: Dirk's face disintegrates and ink outlines break into 4-pixel stair-steps. The 256-colour palettised frame is excellent, so the palette was never the problem -- block VQ was. Replaced it with a Cinepak-style hybrid: each 4x4 block is SKIP, one 4x4 codeword, four 2x2 codewords, or RAW literal pixels, chosen per block by rate-distortion. The RAW escape makes lam=0 pixel-exact (measured 0.00 dB loss), so the quality knob spans lossless to heavily-compressed in one bitstream. Per the user's decision, ships TWO quality profiles from that one codec, one decoder and one bitstream -- only the rate knob differs: sasi 45 KB/s lam=300 34.8 dB stock 10MHz ACE/EXPERT scsi 75 KB/s lam=100 35.9 dB Super/XVI or CZ-6BS1 Three corrections to earlier numbers: 1. Session 1's "183 KB/s at 12fps" was a bad extrapolation. Halving the framerate does not halve the bitrate -- decimation roughly doubles the per-frame delta. Re-measured directly: 340 KB/s for session 1's own RLE, 247 KB/s for changed-spans+deflate. The lossless floor is 319 MB. 2. A FOURTH false-good result, same family as the three in FINDINGS 4: k=1024 codebooks appeared to buy +2.4 dB free, because the rate model charged 1 byte for a 10-bit index. Charging the true cost reverses the verdict -- k=256 wins at every matched bitrate, and by 5 dB at the low end where the SASI profile lives. k=256 ships. 3. Stream inventory: the ~3-5MB clips are 1.2-1.7s, not ~60s, and some 60s streams are menus, not content. Any survey must classify before averaging. Also cleared both candidate sources for the game-logic layer: the SNES project is MIT and DirkSimple is zlib, so the arcade scene graph can be imported and the two transcriptions diffed against each other. Encoder is working end-to-end: extract.py -> vq/vq_hybrid/ratectl -> encode.py, emitting a big-endian DLX1 container the 68000 can parse with plain moves. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6 |