diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index 06b341e..7fbc20d 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -792,3 +792,101 @@ and the 38% full-frame blit estimate underpinning the CPU budget remains unvalidated. What this section adds is that the *target mode* is now real, so 68000 code has a defined geometry to write into: 256 words per visible row, a 1024-byte line stride, and rows 32..223 of a 256-row page. + +--- + +## 24. The blit, measured on the 68000 — the 38% estimate was wrong (session 5) + +**The first 68000 instructions in this project to draw a pixel.** Everything in +22 and 23 was GVRAM filled from Lua, which costs zero 68000 cycles. This section +replaces the estimate that the whole CPU budget rested on with a measurement. + +Harness: `tools/bench/blit.s` + `tools/bench/blit.lua`. Four variants of a +full-frame 256x192 paint, each looped to run ~4 emulated seconds, timed from +`machine.time` between two flag writes by the 68000 itself. + +| variant | what it does | cycles/frame | % of a 12fps frame | +|---|---|---:|---:| +| **V1** | `movem.l` blit from a word-expanded RAM frame (96KB read + 96KB write) | **446,286** | **53.6%** | +| V2 | naive `move.b`/`move.w` per pixel from a byte source | 1,284,174 | 154.1% | +| **V3** | write-only floor — registers preloaded, no source read at all | **225,789** | **27.1%** | +| **V4** | the same 96KB of writes issued in **4x4 block order** | **637,971** | **76.6%** | + +The 12fps budget is 833,333 cycles (10.0 MHz confirmed from `x68k.cpp:1133`, +`40_MHz_XTAL / 4`). + +### 24.1 The numbers are cross-checked against hand-derived cycle counts +Every variant was predicted from the MC68000 timing tables *before* the run +(`MOVEM.L` M->R `(An)+` = 12+8n, `(d16,An)` = 16+8n; R->M `(An)` = 8+8n, +`(d16,An)` = 12+8n) and then measured: + +| | predicted | measured | error | +|---|---:|---:|---:| +| V1 | 447,744 | 446,286 | 0.33% | +| V2 | 1,284,096 | 1,284,174 | 0.006% | +| V3 | 225,792 | 225,789 | 0.001% | +| V4 | 640,704 | 637,971 | 0.43% | + +This agreement is the point. A MAME timing number on its own would be worth +little given how many false-good results this project has produced (FINDINGS 4); +two independent derivations landing within half a percent is worth something. +The residual error is the frame-granularity of the measurement — Lua gets no +cycle counter (`luaengine.cpp` exposes `machine.time` and nothing from +`device_execute_interface`), so timing resolution is one video frame, 18.03 ms. + +### 24.2 SCOPE: these are instruction cycles, and therefore a LOWER BOUND +MAME's `gvram_w`/`gvram_r` (`x68k_crtc.cpp:501,595`) contain **no timing at +all** — no wait states, no `adjust_icount`. GVRAM in MAME is as fast as main +RAM. Real X68000 GVRAM stalls the CPU on access, so every figure above is a +floor, not a prediction. **Do not quote these as hardware numbers.** Interrupts +were masked (`SR = $2700`) so the IPL's timer and VBL handlers could not steal +cycles into the measurement; a real player will take interrupts on top. + +### 24.3 The 38% estimate is dead — a full-frame blit is 53.6% +The realistic "decode into a RAM frame, then blit it" design costs **53.6% of +the frame budget before decoding a single block**, and that is the zero-wait- +state floor. The estimate the CPU budget has been carrying since session 1 was +38%. It was optimistic by 41%. + +The cause is visible in the V1/V3 gap: **reading the source frame is exactly +half the total cost** (221,952 of 446,286 cycles). The 68000 pays 8 cycles per +longword read and 8 per longword written, and in 256-colour mode a pixel +occupies a whole word of address space, so a frame is 96KB of traffic in each +direction rather than 48KB. + +### 24.4 The high byte of every GVRAM write is discarded — confirmed from source +`gvram_w` case `0x0100` writes `data & 0x00ff` with `mem_mask 0x00ff`. So in +256-colour mode the CPU cannot pack two pixels into one word, and the odd bytes +of a word-expanded source frame never need clearing — V1 exploits this by +leaving them uninitialised. This is why 96KB, not 48KB, is the irreducible +write traffic. + +### 24.5 The architecture question, and where it turns over +V4 prices the access pattern a decoder that writes codewords **straight into +GVRAM** actually has: 4 rows of 8 bytes at a 1024-byte stride per 4x4 block. The +same 96KB of writes costs **76.6%** in block order versus 53.6% row-linear — the +stride destroys the `movem.l` burst, 208 cycles per block against a theoretical +best of ~150. + +But a decoder never writes every block: SKIP blocks cost **nothing at all**, and +the previous frame is already sitting in GVRAM, so **no RAM reference frame is +needed for SKIP to work**. So the two designs scale differently: + +- **compose-in-RAM then blit** — flat 53.6%, independent of how much changed +- **decode-direct-to-GVRAM** — 76.6% x (fraction of non-SKIP blocks) + +**They cross at 70% of blocks changed.** Below that, writing straight into GVRAM +wins, and it also drops the 96KB RAM reference frame entirely. Above it, the +flat blit wins. + +**This makes the non-SKIP block fraction the single most important unmeasured +number in the project.** It is already computable from the encoder — it is a +by-product of the mode decision in `vq_hybrid.py` — and it has never been +reported. Measure it before writing any decoder inner loop, because it selects +which inner loop to write. + +### 24.6 The frame the 68000 drew is pixel-exact +V1's output was snapshotted and passes `verify_frame256.py` unchanged: `256x512 +native, double-scan exact, active 256x192 pixel-exact, letterbox true black`, +40.81 dB. So 68000 code drives the mode of FINDINGS 23 correctly, and 23.5 is +now closed. diff --git a/docs/STATUS.md b/docs/STATUS.md index 2f29419..99d5437 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,4 +1,4 @@ -# Status & next-session handoff — end of session 4 (2026-08-23) +# Status & next-session handoff — end of session 5 (2026-08-23) ## Start here: is the tree still green? @@ -82,6 +82,29 @@ rate-distortion curve, not two codecs. the working-setup section below. They cost ~1.5 h of wall clock and a wedged CPU core, and one of them was hit again this session. +## What session 5 settled + +1. **68000 code drew a frame, and the blit was measured.** `tools/bench/blit.s` + + `blit.lua`. The snapshot passes `verify_frame256.py` unchanged — pixel-exact + in the real 256x256 mode. **FINDINGS 23.5 is closed**: no longer "proven from + Lua only". +2. **The 38% full-frame blit estimate is dead. It is 53.6%.** And that is a + zero-wait-state floor — MAME models no GVRAM wait states, so real hardware is + worse. FINDINGS 24. Every variant was hand-derived from the MC68000 timing + tables before being measured and the two agree to 0.006-0.43%, so this is not + another MAME artefact. +3. **Reading the source frame is exactly half the blit cost** (V1 53.6% vs a + write-only floor V3 of 27.1%). That is what makes the architecture question + below live. +4. **The decoder architecture now hinges on one unmeasured number.** Writing + codewords straight into GVRAM costs 76.6% of the frame budget for a *full* + frame (V4 — the 1024-byte stride kills the `movem.l` burst), but scales with + the non-SKIP block fraction and needs **no RAM reference frame at all**, + because the previous frame is already in GVRAM. Compose-then-blit is a flat + 53.6%. **They cross at 70% of blocks changed.** FINDINGS 24.5. + +--- + ## What session 4 settled 1. **A real 256x256 CRTC mode exists and is verified.** `crtc_mode.lua`, derived @@ -212,16 +235,15 @@ functional models, not timing-accurate; a KB/s figure from MAME measures the emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach (MAME validates the path, derivation bounds it, real hardware settles it). -## Display path — VERIFIED (session 3), in a real mode (session 4). CPU path — still unproven. +## Display path — VERIFIED (session 3), in a real mode (session 4), by 68000 code (session 5). The first real frame is on screen: `docs/images/x68k_first_frame_compare.png`. -**What this does and does not mean.** The video hardware is genuinely emulated -and the render is bit-exact. But GVRAM was filled by a MAME Lua script, not by -68000 code — no 68000 instruction has drawn a pixel yet. Lua writes cost zero -68000 cycles, so the 38% full-frame blit estimate underpinning the whole CPU -budget is still unvalidated. "Verified end to end" applies to the *display* -path only. See FINDINGS 22 scope note. +**Session 5 closed the gap this paragraph used to describe.** GVRAM is now +filled by 68000 instructions and the result is still pixel-exact, and the blit +cost is measured rather than estimated: **53.6% of a 12fps frame**, not 38% +(FINDINGS 24). The remaining caveat is different and narrower: MAME models +**no GVRAM wait states**, so 53.6% is a floor and real hardware is worse. Full write-up in **FINDINGS 22**. Harness: `tools/bench/show_frame.lua` + `tools/bench/prep_frame.py`. @@ -262,53 +284,53 @@ SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \ ## Next steps, in priority order -1. **Full-disc survey.** Only 4 clips of 1.2-1.7 s out of 224 streams have been +1. **Measure the non-SKIP block fraction.** *(new top priority, session 5)* + FINDINGS 24.5: compose-in-RAM-then-blit costs a flat 53.6% of the frame + budget; decode-direct-to-GVRAM costs 76.6% x (fraction of blocks that are not + SKIP) and needs no RAM reference frame. **They cross at 70%.** Which side of + 70% the content sits on decides which decoder inner loop to write, so this + must come before writing one. + **It needs no new machinery** — the mode decision in `vq_hybrid.py` already + computes it per frame and simply never reports it. Add the histogram + (SKIP / V1 / V4 / RAW counts per frame) to `encode.py` output and run it over + the clips already extracted. Report the *distribution*, not the mean: a + scene-cut frame is ~100% non-SKIP and a held frame near 0%, and the mean of + those two is a number describing no actual frame. + +2. **68000 decoder skeleton**, with the inner loop chosen by (1). Parse `DLX1`, + expand codebooks, blit per block mode. The display path is verified *by 68000 + code* now (FINDINGS 24) and the harness pattern is `tools/bench/blit.s` + + `blit.lua`, which already loads code, masks interrupts, times a loop against + a flag, and snapshots the result for `verify_frame256.py`. Copy that. + Assembler: `tools/vasm/vasmm68k_mot -Fbin -o out.bin in.s`. + +2a. **Re-budget everything against 53.6%, not 38%.** Several downstream figures + were derived from the old estimate. The blit alone now eats over half the + frame at 12fps in the compose-then-blit design, before any decode, and MAME + models no GVRAM wait states so that is a floor. This may reopen questions + that were closed against the 38% number — check FINDINGS 17.2's entropy-coding + rejection, which was argued as "54% LZ4 with no room beside a 38% blit". The + conclusion gets *stronger*, not weaker, but the arithmetic should be restated. + +3. **Full-disc survey.** Only 4 clips of 1.2-1.7 s out of 224 streams have been measured, and 00146 already runs 23% hotter than 00020. A *sustained* action sequence is the one thing that could still break the bitrate. Classify menu vs content first (FINDINGS 13) or the averages are diluted by static menus. **Vectorise `_paint` before this run** — it is a Python per-block loop. -2. **68000 decoder skeleton.** Parse `DLX1`, expand codebooks to word-per-pixel, - blit SKIP/V1/V4/RAW. Measure real cycles with the existing MAME Lua harness. - **Fully unblocked** — the display path is verified (FINDINGS 22) AND the - target CRTC mode is now real (FINDINGS 23), so 68000 code has a defined - geometry to write into: 256 words per row, 1024-byte line stride, picture in - rows 32..223 of a 256-row page. `tools/bench/show_frame256.lua` gives a - known-good reference image to diff the 68000's output against. This is what - validates the 38% full-frame blit estimate the whole CPU budget rests on. - **This is now the top priority** — it is the only remaining unknown that can - still invalidate the design. + Pairs naturally with (1): the same run produces both numbers. - **Concrete first step, deliberately smaller than "write the decoder":** do - not start by parsing `DLX1`. Start by making 68000 code do the dumbest - possible full-frame blit — copy 256x192 bytes from RAM to GVRAM through the - mode set by `crtc_mode.lua` — and time it with the existing Lua harness. - That single number either confirms or kills the 38% estimate, and it needs - no bitstream, no codebooks, and no container parsing. `show_frame256.lua` - already produces the exact reference image to diff the result against, and - `verify_frame256.py` already knows how to check it. Only once that number is - in hand is it worth writing the mode dispatch. - - Assembler: `tools/vasm/vasmm68k_mot -Fbin -o out.bin in.s`. The harness - pattern for loading and running 68000 code is in `tools/bench/one.lua` and - `tools/bench/bench.lua` (working, from session 1). -2a. ~~CRTC mode table for 256x192-in-256x256.~~ **DONE, session 4.** Derived from - the CRTC divisor ladder (not recalled), verified by snapshot, pixel-exact. - `tools/bench/crtc_mode.lua`; write-up in FINDINGS 23; regression test - `tools/bench/verify_frame256.py`. Untested on real hardware, but the blanking - timing is identical to the IPL's 768 mode, which is what a monitor cares about. - -3. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but +4. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but it is what gives a deterministic ceiling over content not yet measured, which was the original reason for choosing VQ. Insurance, not a fix. Pairs with (1). -4. **Confirm DMA vs PIO in MAME** (see the benchmark section above) — cheap, and +5. **Confirm DMA vs PIO in MAME** (see the benchmark section above) — cheap, and the only thing that could still move CPU into the binding position. -5. **Resolve the framing question** (FINDINGS 12: crop vs squash vs wide). +6. **Resolve the framing question** (FINDINGS 12: crop vs squash vs wide). Needs an eyeball against arcade reference, not a measurement. -6. **Import the scene graph.** SNES project `data/events/` (MIT, cleared), +7. **Import the scene graph.** SNES project `data/events/` (MIT, cleared), cross-checked against DirkSimple (zlib) which transcribed the same data independently — diff them to catch transcription errors before committing any of it to 68000 tables. -7. **ADPCM audio.** MSM6258, 15.6kHz mono, 7.8 KB/s — already budgeted in +8. **ADPCM audio.** MSM6258, 15.6kHz mono, 7.8 KB/s — already budgeted in `ratectl.py`, not yet extracted or encoded. ### Explicitly abandoned — do not re-propose @@ -321,8 +343,9 @@ SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \ - ~~Flat 4x4 VQ.~~ Rejected by eye (FINDINGS 9). ## Not yet started -- **Any 68000 player code.** `src/player/` is still empty. The display path is - proven, but proven *from Lua* — no 68000 instruction has yet drawn a pixel. +- **Any 68000 player code.** `src/player/` is still empty. 68000 code has now + drawn a frame, but it lives in `tools/bench/blit.s` as a benchmark, not in a + player: it does no bitstream parsing, no mode dispatch, no codebook expansion. - ADPCM audio extraction/encoding - Disk image packaging - Game logic (scene branching, input windows, death clips) @@ -384,3 +407,21 @@ with `extract.py`; the earlier ones lived in `/tmp` and do not survive a reboot. Stern's scene boundaries the way `DRAGONS_LAIR.iso` is, so the footage would have to be sourced and cut to match. Not to be started until the CPU path is proven — it changes nothing about whether this design works. + +## Reproducing the blit measurement (session 5) + +``` +python3 tools/encoder/extract.py 00020 tmp/fr_00020 12 crop +python3 tools/bench/prep_frame.py tmp/fr_00020 tmp/frame256.bin 0 --reserve-black +tools/vasm/vasmm68k_mot -Fbin -o tmp/blit.bin tools/bench/blit.s +mkdir -p tmp/snap_blit && cd tmp && SDL_VIDEODRIVER=dummy timeout -k 5 900 mame x68000 \ + -bios ipl10 -video soft -window -sound none -nothrottle -plugins \ + -autoboot_script ../tools/bench/blit.lua \ + -snapshot_directory ./snap_blit -snapview native -seconds_to_run 120 +``` +~25 s wall. Prints cycles/frame and % of a 12fps budget for V1-V4, and snapshots +V1's output. To check that snapshot is still pixel-exact: +`sed 's|snap256|snap_blit|' tools/bench/verify_frame256.py | python3 -` + +Not added to `check.sh`: `check.sh` asserts pixel-exactness, and asserting wall +timings there would make the green-light check sensitive to host load. diff --git a/tools/bench/blit.lua b/tools/bench/blit.lua new file mode 100644 index 0000000..4222552 --- /dev/null +++ b/tools/bench/blit.lua @@ -0,0 +1,159 @@ +-- Time the full-frame GVRAM blit (tools/bench/blit.s) on the emulated 68000. +-- +-- This is the first measurement in the project where 68000 instructions, not +-- Lua, put the pixels on screen. It validates (or kills) the 38% full-frame +-- blit estimate the whole CPU budget rests on. +-- +-- MEASUREMENT SCOPE. MAME's gvram_w/gvram_r (x68k_crtc.cpp:501,595) contain +-- no timing whatsoever -- no wait states, no icount adjustment. So what is +-- measured here is pure 68000 instruction cycles against zero-wait-state +-- memory. Real X68000 GVRAM stalls the CPU; every number below is therefore +-- a LOWER BOUND, not a prediction. Interrupts are masked (SR=$2700) so the +-- IPL's timer and VBL handlers cannot steal cycles into the measurement. +-- +-- Timing resolution is one video frame (1/55.46 s = 18.03 ms), because Lua +-- gets no cycle counter -- luaengine.cpp exposes machine.time and nothing +-- from device_execute_interface. Each variant therefore loops enough times +-- to run ~4 emulated seconds, putting the granularity error near 0.4%. + +M = manager.machine +SP = M.devices[":maincpu"].spaces["program"] + +local function load_mode() + for _,p in ipairs{"../tools/bench/crtc_mode.lua","tools/bench/crtc_mode.lua","crtc_mode.lua"} do + local f = loadfile(p); if f then return f() end + end + error("crtc_mode.lua not found") +end +local MODE = load_mode() + +local FLAG, VAR, ITER = 0x18000, 0x18004, 0x18008 +local SRCW, SRCB = 0x60000, 0x80000 +local GVRAM, GPAL = 0xC00000, 0xE82000 +local CPUHZ = 10000000 -- x68k.cpp:1133, 40_MHz_XTAL/4 +local FRAME12 = CPUHZ / 12 -- 833333 cycles at 12 fps + +-- Iteration counts sized so every variant runs ~4 emulated seconds. +local PLAN = { + {var=1, iter=100, name="V1 movem.l blit from word-expanded RAM (96KB read + 96KB write)"}, + {var=2, iter= 50, name="V2 naive byte-source expansion (move.b/move.w per pixel)"}, + {var=3, iter=200, name="V3 write-only floor (no source read at all)"}, + {var=4, iter= 60, name="V4 same 96KB of writes, issued in 4x4 BLOCK order (decoder access pattern)"}, +} + +local code do + local f = io.open("blit.bin","rb"); code = f:read("a"); f:close() +end + +local frame do + local f = io.open("frame256.bin","rb"); frame = f:read("a"); f:close() +end +local function B(i) return string.byte(frame,i) end +local W, H = B(5)*256+B(6), B(7)*256+B(8) +local PAL0, PIX0 = 9, 9+256*3 +local YOFF = (MODE.height - H) // 2 + +-- Identical packing to show_frame256.lua: shared LSB I chosen per entry. +local function pal6(v) return ((v<<2)|(v>>4)) & 0xff end +local function pack(r,g,b) + local f = {r>>3, g>>3, b>>3} + local best, bestI = nil, 1 + for I = 0,1 do + local e = 0 + for c = 1,3 do + local want = ({r,g,b})[c] + local d = pal6((f[c]<<1)|I) - want + e = e + d*d + end + if best == nil or e < best then best, bestI = e, I end + end + return (f[2]<<11)|(f[1]<<6)|(f[3]<<1)|bestI +end + +local function T() local t = M.time; return t.seconds + t.attoseconds/1e18 end +local function P(s) print("[BLIT] "..s) end + +local function setup() + MODE.apply(SP) + -- letterbox rows: GVRAM holds IPL leftovers, not zeros + for y = 0, MODE.height-1 do + if y < YOFF or y >= YOFF+H then + local base = GVRAM + y*1024 + for x = 0, MODE.width-1 do SP:write_u16(base + x*2, 0) end + end + end + for c = 0, 255 do + local o = PAL0 + c*3 + SP:write_u16(GPAL + c*2, pack(B(o), B(o+1), B(o+2))) + end + -- Source frames in main RAM. SRCW holds one pixel per WORD with the index + -- in the low byte; the high byte is left as-is because gvram_w masks it off. + for y = 0, H-1 do + local row = PIX0 + y*W + for x = 0, W-1 do + local px = B(row+x) + SP:write_u16(SRCW + y*512 + x*2, px) + SP:write_u8 (SRCB + y*256 + x, px) + end + end + for i = 1, #code do SP:write_u8(0x10000+i-1, string.byte(code,i)) end + P(string.format("loaded blit.bin=%d bytes, source frame %dx%d at yoff=%d", #code, W, H, YOFF)) +end + +local step, st, t0, snapped = 0, "boot", nil, false +local results = {} + +local function launch(p) + SP:write_u32(FLAG, 0) + SP:write_u32(VAR, p.var) + SP:write_u32(ITER, p.iter) + local cpu = M.devices[":maincpu"] + cpu.state["SR"].value = 0x2700 -- supervisor, ALL interrupts masked + cpu.state["SP"].value = 0x8000 + cpu.state["PC"].value = 0x10000 + st, t0 = "running", nil +end + +local function report(p, dt) + local cyc = dt * CPUHZ / p.iter + local pct = 100 * cyc / FRAME12 + results[#results+1] = {p=p, cyc=cyc, pct=pct} + P(string.format("%s", p.name)) + P(string.format(" %d iterations in %.4f s -> %.0f cycles/frame = %.1f%% of a 12fps frame", + p.iter, dt, cyc, pct)) +end + +SUB = emu.add_machine_frame_notifier(function() + local ok, err = pcall(function() + local t = T() + if st == "boot" then + if t < 3.0 then return end + setup(); step = 1; launch(PLAN[1]); return + end + if st == "running" then + local fl = SP:read_u32(FLAG) + if fl == 1 and not t0 then t0 = t; return end + if fl == 0xFF then + report(PLAN[step], t - (t0 or t)) + if step == 1 and not snapped then st, snapped = "snap", true; return end + step = step + 1 + if PLAN[step] then launch(PLAN[step]) else st = "finish" end + return + end + if t > 60 then P("TIMEOUT flag="..string.format("%08X",fl)); M:exit() end + return + end + if st == "snap" then + M.video:snapshot(); P("snapshot taken after V1 -- 68000-drawn frame") + step = step + 1; launch(PLAN[step]); return + end + if st == "finish" then + P("---- summary (instruction cycles only; real GVRAM adds wait states) ----") + for _,r in ipairs(results) do + P(string.format(" V%d %8.0f cyc %5.1f%% of 12fps frame", r.p.var, r.cyc, r.pct)) + end + M:exit() + end + end) + if not ok then print("[BLIT] LUA ERROR: "..tostring(err)); M:exit() end +end) diff --git a/tools/bench/blit.s b/tools/bench/blit.s new file mode 100644 index 0000000..1b9053d --- /dev/null +++ b/tools/bench/blit.s @@ -0,0 +1,156 @@ +; Full-frame GVRAM blit cost on a stock 68000 @ 10MHz. +; +; Answers: what fraction of a 12fps frame budget (833,333 cycles) does simply +; PUTTING a decoded 256x192 frame on screen cost, before any decoding? +; +; Geometry (tools/bench/crtc_mode.lua): 256-colour page, one pixel per WORD of +; CPU address space, 1024-byte line stride, picture in rows 32..223 of a +; 256-row page. So a row is 512 contiguous bytes of writes, then a 512-byte +; skip. 192 rows = 98,304 bytes of GVRAM write traffic per frame. +; +; Confirmed from MAME 0.277 x68k_crtc.cpp:501 (gvram_w, case 0x0100): a CPU +; write in 256-colour mode is masked to 0x00ff, so the HIGH byte of every word +; written is discarded by the hardware. V1 exploits this -- it never has to +; clear the odd bytes of its source. +; +; Three variants, selected by VAR, each looped ITER times: +; V1 movem.l blit from a word-expanded RAM frame (96KB). The realistic +; "decode to RAM, then blit" design. Reads 96KB, writes 96KB. +; V2 naive byte-source expansion (move.b / move.w per pixel). The obvious +; implementation, kept as the baseline V1 has to beat. +; V3 write-only floor: registers preloaded once, no source read at all. +; Nothing that puts this many pixels on screen can beat V3. The gap +; V1-V3 is the price of reading a source frame at all. +; V4 the SAME 96KB of writes, but issued in 4x4 BLOCK order instead of +; row-linear order. This is the access pattern a decoder that writes +; codewords straight into GVRAM actually has, and it is the number that +; picks the decoder architecture: compose-in-RAM-then-blit (V1) versus +; decode-direct-to-GVRAM (V4 scaled by the fraction of non-SKIP blocks). +; Each block is 4 rows of 8 bytes at a 1024-byte stride, so the +; destination displacements 0/1024/2048/3072 all fit a 16-bit offset and +; the block needs only one base pointer. V4 deliberately scrambles the +; picture (it reads a row-linear source in block order); it is a timing +; probe, which is why the correctness snapshot is taken after V1. +; +; 12 registers per movem burst (d0-d7/a2-a5 = 48 bytes) is the maximum +; available: a0=src, a1=dst, a6=end sentinel. The row counter lives in the +; a1-vs-a6 compare rather than a d-register for exactly this reason. +; 512 = 10*48 + 32, hence ten 12-register bursts and one 8-register tail. +; Destination uses (d16,a1) displacement rather than post-increment because +; movem cannot post-increment a destination; the displacement costs 4 cycles +; per burst but saves an 8-cycle lea, so it is the cheaper of the two. + +FLAG = $18000 ; 0 idle / 1 running / $FF done +VAR = $18004 ; variant selector, written by Lua +ITER = $18008 ; iteration count, written by Lua +SRCW = $60000 ; word-expanded frame 192*512 = 96KB +SRCB = $80000 ; byte-per-pixel frame 192*256 = 48KB +DST0 = $C08000 ; GVRAM + 32*1024 (first picture row) +DSTE = $C38000 ; GVRAM + 224*1024 (one past last) + + org $10000 +start: + move.l VAR.l,d0 + move.l #1,FLAG.l ; timer starts here + cmp.l #1,d0 + beq v1 + cmp.l #2,d0 + beq v2 + cmp.l #4,d0 + beq v4 + bra v3 + +; ---------------------------------------------------------------- V1 +v1: lea SRCW,a0 + lea DST0,a1 + lea DSTE,a6 +v1row: movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,48(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,96(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,144(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,192(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,240(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,288(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,336(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,384(a1) + movem.l (a0)+,d0-d7/a2-a5 + movem.l d0-d7/a2-a5,432(a1) + movem.l (a0)+,d0-d7 + movem.l d0-d7,480(a1) + lea 1024(a1),a1 + cmpa.l a6,a1 + bne v1row + subq.l #1,ITER.l + bne v1 + bra done + +; ---------------------------------------------------------------- V2 +v2: lea SRCB,a0 + lea DST0,a1 + lea DSTE,a6 +v2row: move.w #255,d1 +v2px: move.b (a0)+,d0 + move.w d0,(a1)+ ; high byte is discarded by gvram_w + dbra d1,v2px + lea 512(a1),a1 ; skip the unused half of the line + cmpa.l a6,a1 + bne v2row + subq.l #1,ITER.l + bne v2 + bra done + +; ---------------------------------------------------------------- V3 +v3: lea SRCW,a0 + movem.l (a0),d0-d7/a2-a5 ; load the burst once, outside the loop + lea DST0,a1 + lea DSTE,a6 +v3row: movem.l d0-d7/a2-a5,(a1) + movem.l d0-d7/a2-a5,48(a1) + movem.l d0-d7/a2-a5,96(a1) + movem.l d0-d7/a2-a5,144(a1) + movem.l d0-d7/a2-a5,192(a1) + movem.l d0-d7/a2-a5,240(a1) + movem.l d0-d7/a2-a5,288(a1) + movem.l d0-d7/a2-a5,336(a1) + movem.l d0-d7/a2-a5,384(a1) + movem.l d0-d7/a2-a5,432(a1) + movem.l d0-d7,480(a1) + lea 1024(a1),a1 + cmpa.l a6,a1 + bne v3row + subq.l #1,ITER.l + bne v3 + bra done + +; ---------------------------------------------------------------- V4 +v4: lea SRCW,a0 + lea DST0,a3 ; base of the current block row + lea DSTE,a4 ; one past the last block row +v4brow: move.l a3,a1 + lea 512(a3),a5 ; 64 blocks * 8 bytes +v4blk: movem.l (a0)+,d0-d7 ; 32 bytes = one 4x4 block, expanded + movem.l d0-d1,(a1) + movem.l d2-d3,1024(a1) + movem.l d4-d5,2048(a1) + movem.l d6-d7,3072(a1) + addq.l #8,a1 + cmpa.l a5,a1 + bne.s v4blk + lea 4096(a3),a3 ; next block row is 4 picture lines + cmpa.l a4,a3 + bne v4brow + subq.l #1,ITER.l + bne v4 + bra done + +done: move.l #$FF,FLAG.l ; timer stops here +halt: bra.s halt