Put the player on a real volume, and find the write window is the frame

ROADMAP K3. src/player/packed.s (2,898 B) brings up its own display, builds
its own 193-entry DMA chain, keeps its own frame clock off V-DISP and fetches
every record itself with READ(10) off a CZ-6BS1. The rig writes no picture
byte, no palette entry and no CRTC register.

120 of 120 frames pixel-exact, every one compared, in both palette orders --
the gate had to grow to do it, because a packed frame is a LITERAL and the
codec's recursion was what made one comparison audit 120.

And the write window turns out to be the frame. A packed write requires R20
bit 11, buffer mode blanks the layer, and a DMAC-direct player holds the
window open for the whole data phase, so

    dark fraction of a slot = record bytes / (DATA-PHASE rate x slot)

which is 1.0 at the container's own 582.0 KB/s: every frame delivered, on
time, pixel-exact, and none of them displayed. The rate in that expression is
the BURST rate, a third hardware number B1 has no test for. It reverses 61.5's
ranking -- a packed player that DMAs to RAM and paints with the measured 27.3%
blit is on screen 72.7% of every slot at any rate, and the two are equal only
at 2,131 KB/s = 3.7x the wire.

And a held channel costs the frame clock half its ticks without the clock
being able to tell: 487 of 1,038 V-DISP edges lost, zero late frames reported,
the player believing 12 fps while the screen ran at 6.37.

FINDINGS 64. ROADMAP K4 opened and fenced behind B2.
check.sh ALL GREEN before and after.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 09:10:48 -07:00
parent f1007a0dbc
commit 6f698ca226
14 changed files with 2091 additions and 23 deletions
+178
View File
@@ -6590,3 +6590,181 @@ resource the packed branch is short of. The per-frame palette is worth +2.31 dB
container by a 68000 or by a channel. That is **K3**, and 63.2 is the strongest
statement available without it: the bytes are right, on a second emulator's
own GVRAM model, with the harness computing nothing.
---
## 64. The packed player runs end to end off the disc — and the write window is the frame (session 32)
**ROADMAP K3.** `src/player/packed.s` is **2,898 bytes** of 68000 code that
brings up its own display, builds its own 193-entry DMA chain, keeps its own
frame clock off V-DISP and fetches every record itself with `READ(10)` off a
CZ-6BS1. `tools/bench/packed.lua` writes **no picture byte, no palette entry and
no CRTC register**; it pushes the code and eleven mailbox words and then reads.
`tools/bench/packed_run.sh` is the gate, `tools/bench/verify_packed.py` the
comparison, `tools/analysis/31_display_duty.py` the arithmetic underneath the
result.
**THE HEADLINE, and it is two facts that point opposite ways.**
**120 of 120 frames are pixel-exact**, every one of them compared, off a real
volume, on a clock the machine keeps itself — the strongest end-to-end result
this project has. And **the picture was on screen for none of the frame slot it
belongs to**, because the write window that a packed frame requires is the whole
of its transfer, and buffer mode blanks the layer it is written through.
### 64.1 What was run, and why every frame had to be checked
`tools/bench/verify_decode.py` checks **one** frame — the last — and that audits
all 120, because the codec is temporally recursive: a SKIP block is a claim that
the previous frame is still in GVRAM. **A packed frame is a LITERAL.** Frame 119
being right says nothing whatever about frame 60. The simplification that
deleted the ring, the codebooks and the decoder also deleted the gate's free
lunch, so `verify_packed.py` snapshots and compares **every frame**, letterbox
included — the 64 static rows are written once at scene setup and never touched
again, so a picture can be pixel-exact while the screen is not.
| | |
|---|---|
| container | `tmp/packed_singe.dlxp`, DLXP1, 120 records of 49,664 B = 97 sectors |
| record `i` | at LBA `1 + i*97`**arithmetic**, no index, nothing walked |
| chain | **193 entries**, built by the 68000: `$E82000`/512 B, then 192 rows of 256 B a 1,024 B stride apart |
| result | **120 of 120 pixel-exact**, 0 frames unsampled, `FLAG=$FF` |
| passes with the palette LAST too | chain starts `$C08000`/256 instead, **120 of 120 pixel-exact** |
The array is **scene-constant** and the seek is **subtraction**: the packed
layout spends both 256-colour pages, so there is no page to flip, and a record's
length is geometry, so a new pass is `LBA0` again. That is the whole of what K3
deletes, and it deleted it without incident.
### 64.2 The write window is the frame, and the rate it needs is not the rate it costs
The free-running run — the one that asks for record *i+1* the instant record
*i* lands, which is what a 12 fps player becomes the moment the transfer is
longer than the slot — reported a number no budget in this tree has a column
for: **the write window was open on 99.5% of the host frames.** Every frame was
pixel-exact and almost none of them was visible.
It is **arithmetic, not an emulator artefact**. 256-colour GVRAM masks the high
byte of every write unless R20 bit 11 is set (46.5/47.1), and the packed
layout's entire 1.0 B/pixel claim is that one word carries two pixels — so a
packed write *requires* the bit. If buffer mode blanks the layer while the bit is
set (47.4/B2 — MAME says it does, 48.1's prior leans that way), the layer is dark
for exactly as long as the window is open, and for a DMAC-direct player the
window is open for the whole data phase. There is no second page to hide behind:
the packed layout spends both, which is the same fact that made a frame one
channel start (62).
dark fraction of a slot = record bytes / (DATA-PHASE rate x slot)
**And the rate in that expression is the BURST rate, not the sustained one.**
This is the correction the session had to make to itself. 582.0 KB/s is a
*sustained* requirement and it decides whether record *i* arrives before slot
*i*. The dark fraction is set by how fast bytes move **during the data phase**,
which for a drive with a read-ahead cache can be several times the sustained
figure. **They are independent, and a medium can pass one and fail the other:**
| requirement | figure | status |
|---|---|---|
| sustained, or frames arrive late | **≥ 582.0 KB/s** | B1, known since 63 |
| data phase, or the frame is never displayed | **see below** | **NEW — B1 has no test for it** |
| data phase | transfer | window open | picture on screen |
|---:|---:|---:|---:|
| 582.0 KB/s (= the wire) | 83.33 ms | 100.0% | **0.0%** |
| 700 KB/s | 69.29 ms | 83.1% | 16.9% |
| 1,164 KB/s | 41.67 ms | 50.0% | 50.0% |
| 2,131 KB/s | 22.75 ms | 27.3% | 72.7% |
| 3,000 KB/s | 16.17 ms | 19.4% | 80.6% |
**A medium that exactly meets the sustained requirement delivers every frame, on
time, pixel-exact, and displays none of them.**
**AND THIS REVERSES 61.5's RANKING.** There are two packed players, and the
difference between them is *when* the window is open:
- **A, DMAC-direct** (the one that is built): one channel start, the CPU halted
or nearly, window open for the whole data phase.
- **B, DMA-to-RAM plus a CPU paint**: the record lands in RAM with the window
shut, and the 68000 paints it with the packed `movem` blit — **227,553 clocks,
27.3% of a slot, MEASURED** (`blit.s` V8, 61.4) and **independent of the
medium**. On screen **72.7% of every slot at any rate that delivers the record
at all.**
They are equally visible at a data-phase rate of **2,131 KB/s, which is 3.7x the
container's own wire**. Below that — which is every rate anyone has proposed —
**the player with the CPU in the loop is on screen longer than the one without
it.** 61.5 is not wrong; it ranked them in **clocks**, and this is the column
that table does not have:
| W (clk per delivered byte) | A: DMAC-direct | B: DMA + CPU paint |
|---:|---:|---:|
| 5 | 31.0% | 58.4% |
| **9 — the dual-address floor** | **54.9%** | **82.2%** |
| 12 | 72.8% | 100.1% |
| 16 | 96.6% | 123.9% |
| 19 | 114.5% | 141.8% |
Both charge the audio DMA at 10,417 clocks (1.25%, from the IPL ROM's own
channel-3 setup, 52.5); neither has a decoder in it. **B costs 27.3% of a frame
and 99,328 B of RAM** — two record buffers, because at any rate near the wire the
delivery of record *i+1* occupies most of the slot the paint of record *i*
happens in. On a 2 MB machine that is 4.7% of memory, and memory is the resource
the packed branch has spare: the ring it deleted was 256 KB.
### 64.3 Holding the bus costs the frame clock half its ticks — and the clock cannot tell
`src/player/clock.i` counts V-DISP interrupts. A held channel **halts the
68000**. The MFP's pending bit is **one bit**, so every edge that falls inside a
transfer spanning two of them is an edge the machine can **never** count. Nothing
in this project had run a transfer and a clock at once, so nothing could have
seen it.
| configuration, 120 frames | V-DISP edges seen | host frames drawn | lost |
|---|---:|---:|---:|
| **held, paced at 12 fps** | 551 | 1,038 | **487 = 46.9%** |
| stealing, paced at 6 fps | 1,105 | 1,112 | 7 = 0.6% |
**And the player reported ZERO late frames in both.** That is not a reassurance,
it is the finding: the pace gate compares the frame index against `PACE`, and
`PACE` is advanced by the ISR the held channel stops the CPU from running — so a
clock that loses edges loses them from **both sides of the comparison**. The held
player believed it was running at 12 fps; the screen was at **6.37**. The only
thing in the run that can contradict it is the host's raster count, which is why
`packed.lua` reports both and `packed_run.sh` gates on the difference being
non-zero.
The CPU's own account says the same thing from the other end: **held, the 68000
went round its transfer wait 120 times in 120 frames — once each, meaning it
never executed during a single transfer.** Stealing, it went round 1,100,520
times. A player has to keep a clock, read a stick and feed ADPCM; **which of the
two configurations can do any of that is a design question, and this is the run
that answers it.**
### 64.4 The channel configuration does not set the transport's time
Free-running, both configurations delivered the same 49,664 B record within 0.5%
of each other: **90.72 ms stealing, 90.27 ms held** — 534.6 and 537.3 KB/s,
108.9% and 108.3% of a 12 fps slot.
**That figure is a property of the apparatus and is not `W` and not a medium.**
MAME's device models carry no transfer timing (`docs/BENCHMARK.md`, 42.5). What
the pair of runs *does* establish is a **negative that no arithmetic could
have given**: the transfer time is **not the DMAC configuration's to set**. What
a channel configuration buys is **who owns the CPU**, not when the picture
appears. The mechanism behind MAME's own ceiling is **not diagnosed** — it is not
the DMAC (the two configurations agree) and not the CPU (held, the CPU is halted
throughout) — and no MAME source tree was available on this machine to name it.
### 64.5 What this does not settle
- **`W`. Not one clock of it.** Unchanged since 59.
- **B2, whether a real board blanks in buffer mode.** Everything in 64.2 is
conditional on it, and the condition now decides **which of two packed players
is built** rather than how much headroom one has. `probe_bit11_blank.lua` is
still written and still wants a board.
- **The data-phase rate of any real medium.** This is the session's addition to
**B1**, and it is a measurement nobody has planned: throughput and seek time
were the two numbers on the list, and **the burst rate during a data phase is a
third** that decides whether a DMAC-direct packed player shows a picture.
- **Whether B is buildable as described.** It is priced off a measured blit and
a measured ladder, and no line of it has been written.