Put the frame clock on the 68000, and find that the 12 fps frame does not exist

ROADMAP P3 said "needs MFP timer or VBL" and neither can do it.  The MFP's
timer clock is 16 MHz/4, its prescalers stop at 200 and its data register is 8
bits, so the slowest tick any single timer can make is 78.125 Hz -- 6.5x faster
than a frame -- and 4e6/12 is not an integer, so no setting reaches 12 Hz at
all.  The raster has no whole divide near 12 either: 4 refreshes is 13.86 fps
and 5 is 11.09.  tools/analysis/23_frame_clock.py walks all 7x256 timer settings
rather than asserting it.

src/player/clock.i takes the V-DISP falling edge on MFP GPIP4 -- the start of
vertical blanking, which is when a player would present -- and adds fps*VTOTAL
per edge to a 16-bit accumulator, emitting a tick at 31,500 and keeping the
remainder.  The long-run rate is fps*VTOTAL/VTOTAL = 12.000000 fps exactly, and
both constants are read out of the CRTC at init, so the clock is derived from
the registers that generate the raster it counts.  Measured over 3,000
refreshes: 3,000 interrupts, 649 ticks where 649.1429 were due.

It costs 181.35 clocks per V-DISP, 838 per frame, 0.1006% of the budget -- timed
by the 68000 itself, because the host's granularity is 17.64 ms and the
interrupt is microseconds.  The loop's own cost was calibrated rather than
looked up and landed on 38.000002 clocks, which both licenses the subtraction
and confirms buscost.py's model; the 181.35 then decomposes exactly, leaving
43.99 clocks for the interrupt exception -- the textbook 44, measured.

THE ONE THAT MOVES SOMETHING: 12 fps on a 55.4577 Hz raster is 4.6215 refreshes,
so a frame is shown for 4 refreshes (72.13 ms) or 5 (90.16 ms), 37.9% of them
short.  The 833,333-clock budget every figure in this project is priced against
is the MEAN slot, and the short one is 13.4% under it.  The cadence was already
in the tree unnamed: stream.lua's tick is sampled at frame boundaries, so its
gaps were always 4 or 5, and every host-paced result in FINDINGS 49/51 carried
it.  P3 moved who produces it onto the machine and made it visible.  It is not a
dropped frame -- the pace gate lets an overrun eat the next frame's idle -- and
on the gate container it costs 4 frames of 120 their idle against 1 for the
nominal model, most of that the frame-0 transient at 111% of budget.  stream.s
counts it now, and the rig matches an offline model of the divider exactly.

Also struck: MAME's raster runs 2.22% fast.  refresh_mode() builds the frame
period from scr.max_x*scr.max_y with scr.max_x = m_htotal - 8, one character
cell short and an inclusive bound used as a count, so it runs at 56.6901 Hz
where the registers say 55.4577 -- agreeing to six digits with the arithmetic.
Every "1/55.46 s granularity" note in this tree was wrong and is 1/56.69 s,
corrected in six files with the derivation put once in crtc_mode.lua.  No
conclusion changes and no 68000 cycle figure moves; the CPU clock is unrelated
to the screen.  But anything paced by the raster runs fast under MAME, so the
rig reports both rates and prices the interrupt against the hardware's.

decode.s and frame.i are unchanged; decode.bin is still 1,296 B at the same MD5.
The pace gate's wait loop is byte-for-byte the one FINDINGS 51 measured and the
free-running path executes none of the new code.  check.sh gains two stages: the
clock's own measurement, and 120 frames decoded pixel-exact with nothing outside
the machine deciding when a frame may start.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-24 20:55:34 -07:00
parent 7179339bd2
commit c419251266
20 changed files with 1513 additions and 23 deletions
+198 -2
View File
@@ -4535,8 +4535,10 @@ because it is a pure copy, and it still runs alone.
**The stages are exactly additive on the exact core.** `P1 + P2 - SCENE = 330`
clocks, and `tables + P1 + P2 - 2x330 = 253,614 = BOOT`, to the clock — 330 is
the front-end's own per-pass overhead. On MAME the same identity closes to 1.8%,
which is one tick of its 1/55.46 s clock over the 0.99 s run. Two instruments,
two granularities, one arithmetic.
which is one tick of its host clock over the 0.99 s run. Two instruments,
two granularities, one arithmetic. (That tick was written here as 1/55.46 s and
is **1/56.69 s** — MAME's raster, not the hardware's, 54.5. 17.64 ms over 990 ms
is 1.78%, so the sentence was right and the label was wrong.)
### 53.3 The scratch tables are scene-independent, so they are not in the scene path
@@ -4618,3 +4620,197 @@ black with `I = 0`. That half of P2 is encoder-side, it changes the container,
and it moves every constant fitted to the gate container, so it is a re-encode
plus a re-measurement rather than an edit. `load.i` is ready for it: it reads
whatever the palette section holds and reports the darkest index either way.
---
## 54. The frame clock moves onto the 68000, and the 12 fps frame turns out never to have existed (session 22)
ROADMAP P3, and the item was phrased "needs MFP timer or VBL" — which quietly
assumes one of those can do it. Neither can, and finding out why produced a
better clock than either and a correction to an instrument the whole tree reads.
`src/player/clock.i` is the clock; `src/player/clockgate.s` and
`tools/bench/clock.lua` measure it; `tools/analysis/23_frame_clock.py`
enumerates the space it was chosen from and prices its cadence. Two new stages
in `tools/bench/check.sh` gate it.
**Layer: MAME 0.277's emulated X68000, not real hardware.** The MFP, the CRTC
and the interrupt sequence are all the emulator's. Where the emulator and the
registers disagree — and they do, 54.5 — the code is built on the registers.
### 54.1 No MFP timer can tick at 12 Hz, and none can tick as slowly as a frame
The MC68901's timer clock on this board is 16 MHz / 4 = 4 MHz
(`sharp/x68k.cpp:1027-1028`), its prescaler ladder is `{4, 10, 16, 50, 64, 100,
200}` (`machine/mc68901.cpp:173`) and its data register is 8 bits. So:
* the **slowest** tick a single timer can produce is 4e6/(200·256) =
**78.125 Hz**, which is 6.5× faster than a 12 fps frame — a software divider
is required whatever the source;
* **4e6/12 = 333,333.33 is not an integer**, so no prescale/data pair divides to
12 Hz at all. `23_frame_clock.py` walks all 7 × 256 of them and finds zero.
A timer clock is therefore not "the simple option". It is a divider *plus* an
interrupt rate 3.6× higher than the raster's, at an arbitrary phase against the
scan.
### 54.2 The raster cannot do it by whole division either — and the fix is exact
V-DISP is on MFP GPIP4 (`x68k.cpp:1139`), and the same pin is Timer A's event
input (`mc68901.cpp:167`, `GPIO_TIMER = {GPIP_4, GPIP_3}`); its interrupt is
channel 6, `IR_GPIP_4 = $40` (`mc68901.cpp:76`). The raster is 31,500/568 =
**55.4577 Hz** exactly. Every whole divide misses:
| Timer A event count | fps | error |
|---:|---:|---:|
| 4 | 13.8644 | +15.54% |
| 5 | 11.0915 | 7.57% |
12 fps needs **4.6215 refreshes per frame**. So the divider keeps a remainder:
```
each V-DISP: acc += fps*VTOTAL ; 12*568 = 6816
if acc >= 31500: acc -= 31500 ; PACE += 1
```
Long-run rate is `fps·VTOTAL/VTOTAL` = **12.000000 fps exactly**, with a
remainder that never accumulates. Both constants are **read out of the CRTC at
init** — `R04+1` for VTOTAL, `R20` bit 4 checked for the 31.5 kHz mode — so the
clock is derived from the registers that generate the raster it counts, and the
two cannot drift apart. The accumulator peaks at 38,316, so it is 16-bit
arithmetic on a 68000; `clk_init` refuses rather than overflow (the ceiling is
fps < 59.9 at this VTOTAL).
**Measured over 3,000 refreshes: 3,000 interrupts, 649 ticks, where 649.1429
were exactly due — an error of 0.14 ticks, i.e. the remainder still held.** The
gate is stated in ticks and not in ppm on purpose: a remainder-keeping divider
is off by at most one tick over *any* window, so quoting ppm would let a longer
window advertise a tighter clock for nothing.
### 54.3 It costs 181.35 clocks per V-DISP — 838 per frame, 0.10% of the budget
The host cannot time this: MAME's Lua sees the machine once per screen frame,
17.64 ms, and the interrupt costs microseconds. So the **68000 times it
itself**. `clockgate.s` runs a one-instruction loop for a window of 3,000
refreshes with the clock off and again with it armed:
```
clock off: iters0·L = clocks in the window -> L
clock on: iters1·L + ints·H = clocks in the window -> H
```
`L` is **calibrated, not looked up** — the point is to price the clock on the
machine rather than against `buscost.py`, which is the table being checked.
| | |
|---|---:|
| loop iteration, calibrated over 13,926,121 of them | **38.000002 clocks** |
| per V-DISP interrupt, over 3,000 | **181.35 clocks** |
| per 12 fps frame (4.6215 interrupts) | **838 clocks = 0.1006%** |
`L` landing on a whole number to seven digits is the check that licenses the
subtraction, and it is also an independent confirmation of `buscost.py`'s model:
`addq.l #1,(xxx).L` is 3 instruction words + 4 data accesses = 7 bus cycles = 28
clocks, plus 10 for the `bra.s`.
**The 181.35 decomposes exactly.** By the same model the handler body is 130
clocks on a V-DISP that emits no tick and 164 on one that does; over the measured
649/3,000 mix that is 137.355, leaving **43.99 clocks for the interrupt exception
sequence** — the textbook 44, measured here rather than recalled.
For comparison, the cheapest exact MFP-timer clock would interrupt 16.7 times a
frame instead of 4.6: **3.6× the cost, for a tick with no fixed relationship to
the scan.**
### 54.4 THE ONE THAT MOVES SOMETHING: there is no 83.33 ms frame, and there never was
12 fps on a 55.4577 Hz raster is 4.6215 refreshes, so a frame is shown for **4
refreshes (72.13 ms) or 5 (90.16 ms)** — 37.9% of them short. The 833,333-clock
budget every figure in this project is priced against is the **mean** slot, and
the short slot is **13.4% under it**.
With the per-frame decode costs (`tmp/c68k_frames.csv`, 120 frames of the gate
container) run through the actual divider and the actual pace gate:
| tick source | short slot | frames over it | no idle left |
|---|---:|---:|---:|
| nominal 1/fps model (no raster has it) | 833,333 | 1/120 | **1/120** |
| the host tick, as `stream.lua` really emits it | 705,590 | 10/120 | **4/120** |
| the 68000's clock, hardware raster | 721,270 | 10/120 | **4/120** |
| the 68000's clock, MAME's raster | 705,590 | 10/120 | **4/120** |
**The cadence was already there and nothing had named it.** `stream.lua`'s tick
is `floor((t - t_rel) * fps)` — which *looks* uniform and is not, because Lua
only sees the machine at frame boundaries, so its ticks land on refreshes and its
gaps are the same two whole numbers. Every host-paced result in FINDINGS 49 and
51 already carried a 4/5 cadence. **P3 did not introduce it. It moved who
produces it onto the machine and made it visible.**
**A short slot is not a dropped frame.** The pace gate says only "not before
tick i", so a frame that overruns spends the next frame's idle and the clock
recovers itself; the cost is one frame presented a refresh late. What the table
counts is frames with no idle left, and the difference between the nominal row
and the raster rows — **1 against 4** — is the entire price of the cadence on
this container.
**The expensive frame is frame 0, at 923,146 clocks = 111% of the nominal
budget**: the first frame of a scene has nothing to SKIP against, so it is the
whole picture in one slot. Most of what follows it in those counts is that
transient draining. It also means the cost lands **at a scene change**, next to
FINDINGS 53.2's 18.96 ms of loader and the seek — not spread over the window.
`src/player/stream.s` now counts this itself (`LATEFR`/`LATEMAX`/`LATE1ST`), and
the rig's count matches the offline model **exactly**: 4/120, first at frame 1,
on both tick sources. The counter sits ahead of the wait loop and the
free-running path executes none of it, so FINDINGS 49's figures are untouched.
### 54.5 MAME's raster runs 2.22% fast, and the whole tree has been sampling it
`x68k_crtc.cpp refresh_mode()` computes the frame period as
`(scr.max_x * scr.max_y)` dots over the dot clock, with
`scr.max_x = m_htotal - 8` — one character cell short, and an **inclusive
rectangle bound used as a count**. In the 256-wide mode that is 360 where the
registers say 368, so MAME's refresh is fast by **368/360 = 1.02222**:
* registers: 31,500/568 = **55.4577 Hz**
* MAME, measured by `clock.lua` over 3,000 frames: **56.6901 Hz**
The two agree to six digits with `clock_69m()/6 / (360·568)`, so this is the
mechanism and not a coincidence. Consequences, and the third one is why it is
worth this much space:
1. **Every "1/55.46 s granularity" note in this tree was wrong** — it is
1/56.69 s, 17.64 ms. Corrected in `decode.lua`, `load.lua`, `span.lua`,
`blit.lua`, `loadgate.s` and `check.sh`, with the derivation put once in
`crtc_mode.lua`. No conclusion changes: 53.2's "one tick over the 0.99 s run"
is 1.78% at the corrected figure and was quoted as 1.8%.
2. **68000 cycle figures are untouched.** The CPU clock is 40 MHz/4 and has
nothing to do with the screen. Nothing in FINDINGS 2453 moves.
3. **A raster-paced player runs 2.22% fast under MAME**, so the rig measures
12.267 fps where the hardware would give 12.000. `clock.lua` reports both and
de-skews, and `clock_run.sh` prices the interrupt against the **hardware**
refresh count — charging the player the emulator's extra interrupts would
overstate the cost by that same 2.2%.
**Do not "fix" 55.4577 to match the measurement.** It is the hardware's, derived
from the dot clocks, and it is what the divider is built on.
### 54.6 What had to be turned off, and why it is in the file
The rigs launch the 68000 at `SR=$2700` into a machine the IPL ROM has already
booted, so the MFP arrives with whatever IOCS enabled on it and vectors pointing
into IOCS. Lowering the mask without disarming it would vector into code we did
not put there. `clk_init` writes `IERA = IERB = 0` first — which on the MC68901
clears the matching pending bits with them (`mc68901.cpp REGISTER_IERA/B`,
`m_ipr &= m_ier`) — then arms GPIP4 alone, takes the falling edge (AER bit 4
clear: the **start of vertical blanking**, which is when a player would present),
and drops to `SR=$2500`. Levels 15 stay masked, so the DMAC (IRQ3) and the SCC
(IRQ5) cannot get in. `VR` is written with **S clear**, so an acknowledge clears
the pending bit by itself and the handler needs no end-of-interrupt write.
The handler saves only the **low word** of `d0`, because every operation in it is
a word operation — which is legal precisely because `clk_init` proved the
accumulator fits 16 bits. `decode.s` and `frame.i` were checked for stack tricks
before the mask was lowered: the only `a7` use in either is one `move.l a1,-(sp)`
pair, so an interrupt cannot corrupt decoder state. The 120-frame self-paced
decode being pixel-exact is the test of that, and it is gated.
+32 -4
View File
@@ -2,6 +2,7 @@
Written end of session 19 (2026-08-24), against a tree that is ALL GREEN.
Amended end of session 21: P1 done, P2 half done (FINDINGS 53).
Amended end of session 22: P3 done (FINDINGS 54).
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
tree — a decision point, two outcomes, a death clip — with audio, streaming from
@@ -124,10 +125,37 @@ re-measurement rather than an edit. Until then the letterbox gets the palette's
closest thing to black (index 255 on the gate container); `load.i` reports
whichever index that is and needs no change when it becomes 0.
**P3. A real frame clock.** `stream.s` has `PACE`/`PACEON` (`$18034`/`$18038`)
but the 12 fps tick comes from the Lua producer. Needs MFP timer or VBL. Keep
`PACEON=0` free-run working — the wrap gate uses it and every FINDINGS 49 figure
depends on it.
~~**P3. A real frame clock.**~~ **DONE, session 22 — FINDINGS 54.**
`src/player/clock.i` derives the tick from the CRTC's own V-DISP through the
MFP, with a remainder-keeping divider whose two constants are read out of the
CRTC at init. **Exactly 12.000000 fps, by construction** — measured at 649 ticks
over 3,000 refreshes where 649.1429 were due, so the remainder still held and
nothing accumulated. **181.35 clocks per V-DISP, 838 per frame, 0.1006% of the
budget**, timed by the 68000 itself because the host's 17.64 ms granularity
cannot see it. `PACEON=0` free-run is untouched and so is the wait loop; the
free-running path executes none of the new code.
The item said "MFP timer or VBL" and **neither can do it alone**: 4e6/12 is not
an integer and no prescale/data pair reaches 12 Hz, while the slowest MFP tick
of any kind is 78.125 Hz; and the raster's 55.4577 Hz has no whole divide near
12 either (4 gives 13.86, 5 gives 11.09). `tools/analysis/23_frame_clock.py`
walks the whole space rather than asserting it.
**What it exposed is bigger than the item.** 12 fps on a 55.4577 Hz raster is
4.6215 refreshes, so a frame gets **4 refreshes (72.13 ms) or 5 (90.16 ms)** and
**there is no 83.33 ms frame** — that figure is the mean slot, and 37.9% of slots
are 13.4% under it. The cadence was ALREADY in every host-paced result in
FINDINGS 49/51, because `stream.lua`'s tick is sampled at frame boundaries and
its gaps were always 4 or 5; nothing had named it. On the gate container it
costs 4 frames of 120 their idle against 1 for the nominal model. **It is not a
dropped frame** — the pace gate lets an overrun eat the next frame's idle and
the clock recovers — but it means every budget in this project is priced against
a slot 37.9% of frames do not get. 54.4.
**Also struck: MAME's raster runs 2.22% fast** (`refresh_mode()` builds the frame
period from `htotal - 8`), so the tree's "1/55.46 s granularity" was 1/56.69 s
throughout. No 68000 cycle figure moves — the CPU clock is unrelated to the
screen — but anything paced by the raster does. 54.5.
**P4. Real transport.** Drive the MB89352 instead of a host file. The `W`
handshake — clocks stolen per delivered byte, bracketed 5..12 by MC68450 Fig
+116 -1
View File
@@ -1,3 +1,117 @@
# Status & next-session handoff — end of session 22 (2026-08-24)
## Session 22: the frame clock moves onto the 68000, and the 12 fps frame turns out not to exist
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this and ALL GREEN after**, 120/120 on both cores, no `TRUNCATED`, plus two
new frame-clock stages.
**ROADMAP P3 is DONE. FINDINGS 54.** P3 was one of the two items session 21's
handoff called buildable here, and it is the one that touches every other number
in the project — because the tick is what the word "frame" in "% of a frame"
means.
**1. Neither of the two sources P3 named can do it, and the enumeration is the
finding.** The MFP's timer clock is 16 MHz/4 = 4 MHz, its prescalers are
`{4,10,16,50,64,100,200}` and its data register is 8 bits, so the **slowest tick
any single timer can make is 78.125 Hz** — 6.5x faster than a frame — and
**4e6/12 = 333,333.33 is not an integer**, so no setting reaches 12 Hz at all.
The raster has no whole divide near 12 either: 4 refreshes is 13.86 fps and 5 is
11.09. `tools/analysis/23_frame_clock.py` walks all 7x256 timer settings rather
than asserting it. 54.1, 54.2.
**2. The clock is the raster with a remainder, and it is exact by construction.**
`src/player/clock.i` takes the V-DISP falling edge on MFP GPIP4 — the start of
vertical blanking, which is when a player would present — and adds `fps*VTOTAL`
per edge to a 16-bit accumulator, emitting a tick at 31,500 and keeping the
remainder. Long-run rate is `fps*VTOTAL/VTOTAL` = **12.000000 fps exactly**.
Both constants are **read out of the CRTC at init**, so the clock is derived from
the registers that generate the raster it counts. Measured: **3,000 interrupts,
649 ticks where 649.1429 were due**. The gate is stated in ticks, not ppm — a
remainder is off by at most one tick over any window, so ppm would let a longer
window advertise a tighter clock for free. 54.2.
**3. It costs 181.35 clocks per V-DISP; 838 per frame; 0.1006% of the budget.**
The host cannot time this — its granularity is 17.64 ms and the interrupt is
microseconds — so **the 68000 times it itself**: a one-instruction loop over a
3,000-refresh window, run with the clock off and on, with the loop's own cost
calibrated rather than looked up. The calibration landed on **38.000002 clocks**
per iteration, which is both the check that licenses the subtraction and an
independent confirmation of `buscost.py`'s model. The 181.35 then decomposes
exactly, leaving **43.99 clocks for the interrupt exception** — the textbook 44,
measured rather than recalled. A timer-based clock would have cost 3.6x this at
an arbitrary phase against the scan. 54.3.
**4. THE ONE THAT MOVES SOMETHING: there is no 83.33 ms frame, and there never
was.** 12 fps on a 55.4577 Hz raster is 4.6215 refreshes, so a frame is shown for
**4 refreshes (72.13 ms) or 5 (90.16 ms)**, 37.9% of them short. The
833,333-clock budget every figure in this project is priced against is the
**mean** slot; the short one is **13.4% under it**, and 10 of the gate
container's 120 frames do not fit it.
**And the cadence was already in the tree, unnamed.** `stream.lua`'s tick is
`floor((t - t_rel) * fps)`, which looks uniform and is not: Lua only sees the
machine at frame boundaries, so its ticks land on refreshes and its gaps were
always 4 or 5. **Every host-paced result in FINDINGS 49 and 51 already carried
this cadence.** P3 did not introduce it; it moved who produces it onto the
machine and made it visible.
**It is not a dropped frame.** The pace gate says only "not before tick i", so an
overrun eats the next frame's idle and the clock recovers itself; the cost is one
frame presented a refresh late. On the gate container that is **4 frames of 120
with no idle left, against 1 for the nominal model** — and the expensive one is
**frame 0 at 111% of budget**, because the first frame of a scene has nothing to
SKIP against. So the cost lands **at a scene change**, next to 53.2's 18.96 ms of
loader and the seek. `stream.s` counts this itself now, and the rig's count
matches an offline model of the divider **exactly**: 4/120, first at frame 1, on
both tick sources. 54.4.
**5. An instrument correction the whole tree was reading.** `x68k_crtc.cpp
refresh_mode()` builds the frame period from `scr.max_x * scr.max_y` with
`scr.max_x = m_htotal - 8` — one character cell short, an inclusive bound used as
a count. **MAME's raster is fast by 368/360 = 2.2222%**: 56.6901 Hz measured
against the registers' 55.4577, agreeing to six digits with the arithmetic. So
every "1/55.46 s granularity" note in this tree was wrong and is **1/56.69 s**;
corrected in six files with the derivation put once in `crtc_mode.lua`. **No
conclusion changes and no 68000 cycle figure moves** — the CPU clock is unrelated
to the screen — but anything *paced* by the raster runs 2.22% fast under MAME, so
`clock.lua` reports both rates and de-skews, and the interrupt is priced against
the hardware refresh count. 54.5.
**New in the tree:** `src/player/clock.i` (the clock) and
`src/player/clockgate.s` (its measurement front-end); `tools/bench/clock.lua`,
`clock_cost.py`, `clock_run.sh` (the rig); `tools/analysis/23_frame_clock.py`
(the enumeration and the cadence pricing). `stream.s` gains `CLKON` and a
late-frame counter ahead of the wait loop; `stream.lua` gains `DLX_PACE=2` and
takes its deadlines from the ticks the machine actually emitted rather than from
a host model; `pace_run.sh` gains `DLX_PACE` selection, with the default tag left
alone so `pace_sweep.sh` still finds its logs. `check.sh` gains two stages: the
clock's own measurement, and 120 frames decoded pixel-exact with nothing outside
the machine deciding when a frame may start.
**`decode.s` and `frame.i` are unchanged.** `decode.bin` is still 1,296 B at the
same MD5. The pace gate's wait loop is byte-for-byte the one FINDINGS 51
measured, and the free-running path executes none of the new code, so every
FINDINGS 49 figure stands.
**Still open in P2:** unchanged — the encoder does not reserve a black entry
(23.4), so the letterbox still gets the palette's closest thing to black.
**Next:** P5 (per-record index, prefill policy, the accumulated-slack rule in the
player rather than the rig) is buildable here and is now the last M2 item that
is. G1 (import the scene graph) still needs fetching, and is still the one that
would let this tree ask what the worst gap between consecutive decision points
is. P4 still decides the project and still cannot be measured here.
**A question 54.4 raises and does not answer:** every rate-control and budget
figure in this project is fitted to an 833,333-clock frame, and 37.9% of frames
get 721,270. Whether the encoder should be fitted to the SHORT slot instead of
the mean is a re-encode plus a re-measurement — the same class of change as the
reserved black entry — and it should be decided with P5's numbers in hand, not
before.
---
# Status & next-session handoff — end of session 21 (2026-08-24)
## Session 21: the loader moves onto the 68000, and a scene change gets a price
@@ -68,7 +182,8 @@ what this project is short of. **Derived, not measured.** 53.6.
gains a `--loadraw` mode, which also makes its flag-watch address a variable
rather than a constant. `check.sh` gains a stage that gates byte-exactness on
both cores, and deliberately does **not** gate the cycle counts — MAME's clock
is 1/55.46 s and a wall timing would make the green light host-sensitive, the
is 1/56.69 s (1/55.46 when that was written; 54.5) and a wall timing would make
the green light host-sensitive, the
same reason `blit.s` and `span.sh` are not in it.
**`decode.s` and `stream.s` are unchanged.** Nothing in the per-frame path was