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:
prosolis
2026-08-23 13:43:56 -07:00
parent 7ba979a236
commit 09a5a50065
4 changed files with 499 additions and 45 deletions
+98
View File
@@ -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 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 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. 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.
+86 -45
View File
@@ -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? ## 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 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. 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 ## What session 4 settled
1. **A real 256x256 CRTC mode exists and is verified.** `crtc_mode.lua`, derived 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 emulator's scheduler. `docs/BENCHMARK.md` covers the three-tier approach
(MAME validates the path, derivation bounds it, real hardware settles it). (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`. 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 **Session 5 closed the gap this paragraph used to describe.** GVRAM is now
and the render is bit-exact. But GVRAM was filled by a MAME Lua script, not by filled by 68000 instructions and the result is still pixel-exact, and the blit
68000 code — no 68000 instruction has drawn a pixel yet. Lua writes cost zero cost is measured rather than estimated: **53.6% of a 12fps frame**, not 38%
68000 cycles, so the 38% full-frame blit estimate underpinning the whole CPU (FINDINGS 24). The remaining caveat is different and narrower: MAME models
budget is still unvalidated. "Verified end to end" applies to the *display* **no GVRAM wait states**, so 53.6% is a floor and real hardware is worse.
path only. See FINDINGS 22 scope note.
Full write-up in **FINDINGS 22**. Harness: `tools/bench/show_frame.lua` + Full write-up in **FINDINGS 22**. Harness: `tools/bench/show_frame.lua` +
`tools/bench/prep_frame.py`. `tools/bench/prep_frame.py`.
@@ -262,53 +284,53 @@ SDL_VIDEODRIVER=dummy mame x68000 -bios ipl10 -video soft -window \
## Next steps, in priority order ## 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 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 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. 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. **Vectorise `_paint` before this run** — it is a Python per-block loop.
2. **68000 decoder skeleton.** Parse `DLX1`, expand codebooks to word-per-pixel, Pairs naturally with (1): the same run produces both numbers.
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.
**Concrete first step, deliberately smaller than "write the decoder":** do 4. **Wire rate control into `encode.py`.** No longer a blocker (FINDINGS 21), but
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
it is what gives a deterministic ceiling over content not yet measured, which 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). 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. 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. 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 cross-checked against DirkSimple (zlib) which transcribed the same data
independently — diff them to catch transcription errors before committing independently — diff them to catch transcription errors before committing
any of it to 68000 tables. 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. `ratectl.py`, not yet extracted or encoded.
### Explicitly abandoned — do not re-propose ### 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). - ~~Flat 4x4 VQ.~~ Rejected by eye (FINDINGS 9).
## Not yet started ## Not yet started
- **Any 68000 player code.** `src/player/` is still empty. The display path is - **Any 68000 player code.** `src/player/` is still empty. 68000 code has now
proven, but proven *from Lua* — no 68000 instruction has yet drawn a pixel. 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 - ADPCM audio extraction/encoding
- Disk image packaging - Disk image packaging
- Game logic (scene branching, input windows, death clips) - 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 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 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. 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.
+159
View File
@@ -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)
+156
View File
@@ -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