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
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user