Import the scene graph, and find the worst gap between two branch points is zero

ROADMAP G1, scheduled early because it is a measurement input, and it paid for
that immediately. 51.3 established that a ring's lookahead is accumulated out of
pipe - wire and that a seek spends all of it, so what a branch point costs is
set by the time since the last one. 55.5 rehearsed a seek on the machine and
said out loud that it could not ask the question, because nothing here knew
where the branch points are.

They are everywhere, and 5.4% of them are free of charge to the player and not
to us. Over 612 distinct transitions into a seek, taking the earliest instant
each input window opens: worst 0.000 s, p10 0.950, median 3.473, best 82.497.
33 open on the first frame of a clip the disc SEEKED to -- press right as
flaming_ropes.enter_room appears and you are in fall_to_death, two seeks with no
play between them. 51.2's slack rule can therefore be answered NO by the
content rather than by the buffer, and no amount of ring is a defence.

It does not break the design. A branch on an empty ring costs the 2-record
prefill, 149.7 ms at 488 KB/s, not the climb. What it removes is margin: at that
rate in a 256 KB ring, 76% of this game's branch points arrive before the ring
has refilled, and a 512 KB ring makes that 90%, because doubling the ceiling
does not touch the surplus. The ring is not the lever; the surplus is.

CORRECTION to FINDINGS 16: there is only one transcription. The SNES chapter
set says in its own README that it is derived from DirkSimple, so the planned
diff of two independent sources catches conversion errors only. Run anyway:
durations agree 388/505 within a frame, branch structure 470/505, and of the 35
differences 16 are renames and 18 of the other 19 are that port dropping the
arcade's diagonals. Zero transcription discrepancies, and none were findable.

Two constraints on the input layer come free: the arcade needs eight directions,
and the shortest input window is 98 ms against 54.4's 72.13/90.16 ms frame slot,
so input cannot be polled on the frame tick.

The coupling to outside source is contained to one file (USER DECISION).
tools/import/scenegraph.py is the only code here that knows those projects
exist -- their paths, table names, timing formulas, constants -- and it writes
DLXSCENE1, this project's own schema, into gitignored tmp/ with the sources'
licences inside it. tools/analysis/25_scene_graph.py reads only that. Nothing is
vendored and nothing outside-derived is committed. The split was made after the
measurement and the whole output was re-run byte for byte to show it moved no
number.

Both import gates are negative-tested: deleting one sequence upstream fails the
516/906 count, and closing the table early fails the constructor-end check,
which replaced one that was vacuous.

No 68000 code ran or changed; decode.bin is still 1,296 B at the same MD5.
check.sh gains an import stage that skips when there is no checkout. ALL GREEN
before and after.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-24 22:11:04 -07:00
parent 2676f3b835
commit 00232bb22b
7 changed files with 1187 additions and 13 deletions
+212
View File
@@ -5017,3 +5017,215 @@ and are exact, so 49.6's table is unaffected.
two-instruction test at the top of the pace wait that routes a self-filled
run into a polling wait loop. The legacy wait loops are byte for byte the ones
FINDINGS 51 measured and a host-filled run executes none of the new code.
---
# Findings — session 24 (2026-08-24)
## 56. The scene graph is in, and the worst gap between two decision points is zero (session 24)
ROADMAP G1, and it was scheduled early *because it is a measurement input*.
FINDINGS 51.3 established that a ring's lookahead is accumulated out of
`pipe - wire` and that a seek spends all of it, so what a branch point costs is
set by the rate and by **the time since the last branch**. 55.5 rehearsed a seek
on the machine and then said out loud that it could not ask the question that
matters, because nothing in this tree knew where the branch points *are*.
Now it does, in two files with a hard line between them (USER DECISION,
session 24):
- **`tools/import/scenegraph.py` is the only file in this tree that knows
anything about somebody else's source.** Their file layout, table names,
timing formulas and magic constants are wired into it and nowhere else. It
writes **`DLXSCENE1`**, this project's own schema — a clip's length, its exits
and the earliest instant each of them can fire — into gitignored `tmp/`.
- **`tools/analysis/25_scene_graph.py` reads only DLXSCENE1** and could not name
an outside project if it wanted to.
Nothing is vendored and nothing outside-derived is committed. That line is worth
drawing before the game-logic layer exists rather than after: when one of those
projects moves, exactly one file in this tree breaks, and when a scene table is
eventually committed rather than regenerated, there is one place the attribution
already lives. Restructuring changed no number in this section — the split was
made after the measurement and the whole output was re-run byte for byte.
### 56.1 What was imported, and what gates it
**40 scenes, 516 sequences, 906 input windows**, plus `scene_manager.rows` — the
13x3 scene order the arcade walks. **282 of the 516 sequences (54.7%) are
entered by a seek**; the rest play on from wherever the disc already is, which
is the distinction the whole finding rests on.
The source table is Lua, and there is no Lua interpreter on this box, so the
importer contains a **deliberately small Lua-subset parser**: table constructors,
literals, bare identifiers, the file's four timing helpers and `+`/`-` between
them. Anything else is a parse error rather than a silent skip. Three gates
stand behind that choice:
1. the four timing helpers are **matched against the text that defines them**
`frame / 23.976`, the `- 6297.0` ROM offset, `noseek` returning `-1`,
`(seconds * 1000) + ms`. If upstream changes a formula, the tool stops
rather than keeps evaluating the old one.
2. the parse must reach **516 sequences and 906 input windows** exactly. A
parser that quietly dropped a branch would produce a *smaller* graph and a
*longer* worst gap, i.e. it would fail in the flattering direction.
3. `check.sh` runs the import and then the analysis, and **skips rather than
fails** when there is no checkout, like every other outside-this-repo input
in the tree.
### 56.2 CORRECTION to FINDINGS 16: there is only one transcription
> 16 cleared two permissively licensed transcriptions and planned to **diff them
> against each other to catch transcription errors**. That plan does not work.
The SNES project's own `data/events/README.md` states its 516 chapter XMLs are
"**derived from DirkSimple game data**". They are a *conversion* of the same
transcription, not a second one. The diff below is still worth running — it
catches conversion errors — but it **cannot** catch a transcription error,
because there is nothing independent to compare against. DirkSimple is the
single source, and its own provenance is the arcade ROM's data table.
That correction was cheap to make and it was one sentence of a README away from
never being made at all. It belongs with FINDINGS 42.1: a plausible source with
no provenance is folklore, and so is a plausible cross-check.
### 56.3 THE MEASUREMENT: the worst gap is zero, and 5.4% of branches are
Chaining play across non-seeking sequences and taking the **earliest** moment an
input window opens (the least play a clip can deliver before the branch it leads
to), over 612 distinct transitions into a seek, excluding attract mode:
| | seconds of play |
|---|---:|
| worst | **0.000** |
| p10 | 0.950 |
| p25 | 1.966 |
| median | 3.473 |
| p75 | 5.800 |
| p90 | 9.548 |
| best | 82.497 |
**33 of the 612 (5.4%) are zero**: an input window that opens at t=0 of a clip
the disc *seeked to*, so two seeks can fall back to back with no play between
them at all. `flaming_ropes.enter_room -> fall_to_death` is one — press right on
the frame the clip starts and the player dies immediately.
This is not an edge case to be designed around; it is the game. **A rule of the
form "has there been enough play since the last branch" — 51.2's slack rule,
`ring_may_seek` in `src/player/ring.i` — can be answered NO by the content,
not by the buffer**, and no amount of ring is a defence.
**203 of the 612 end the scene**, which in this design is also a container
change and needs 6,164 header bytes before frame 0 (53, 55.1). The worst
scene-change gap is **0.541 s** and the median is 2.501 s.
### 56.4 The median branch point arrives before the ring has refilled
51.3's climb, against the game's own gap distribution. Gate container
(`rc_fr_singe_scsi_span.dlx`, mean record 36.5 KB, wire 446.1 KB/s); rates are
explicit arguments and every one of them is a sensitivity, not a claim:
| ring KB | pipe KB/s | ceiling | climb s | branch points under the climb |
|---:|---:|---:|---:|---:|
| 256 | 451.4 | 3 | 20.83 | 601/612 (98%) |
| 256 | 488.0 | 7 | 6.11 | 468/612 (76%) |
| 256 | 513.2 | 7 | 3.81 | 370/612 (60%) |
| 256 | 600.0 | 7 | 1.66 | 129/612 (21%) |
| 512 | 488.0 | 11 | 9.60 | 551/612 (90%) |
| 512 | 513.2 | 14 | 7.63 | 512/612 (84%) |
| 512 | 600.0 | 14 | 3.32 | 301/612 (49%) |
Two things fall out, and the second is the one that costs something.
1. **At every rate this tree has considered, most branch points arrive with less
lookahead than the one before them.** At 488 KB/s in a 256 KB ring — the
configuration `check.sh` gates — that is 76%.
2. **A bigger ring makes this metric worse, and now content says so too.** 51.3
derived it from the surplus alone; here the same rate goes from 76% under the
climb at 256 KB to 90% at 512 KB, because doubling the ring doubles the
ceiling without touching `pipe - wire`. **The ring is not the lever. The
surplus is.**
### 56.5 What a branch actually costs when the gap bought nothing
The climb is what a player needs to *tolerate* the next branch. What it *pays*
at one is the prefill, because the ring is empty after a seek and 55.4's shipped
policy releases the decoder at 2 records:
| pipe KB/s | 2-record prefill | as a scene change (+6,164 B) |
|---:|---:|---:|
| 451.4 | 161.8 ms (1.94 frame slots) | 175.2 ms (2.10) |
| 488.0 | 149.7 ms (1.80) | 162.0 ms (1.94) |
So a zero-play branch is **not** a failure: it costs about two frame slots of
black, every time. What it removes is margin. A player that branches at p10
(0.950 s) has spent its whole lookahead and rebuilt almost none of it, and the
next slow record has nothing behind it. **The finding is not "this breaks", it
is "this design runs permanently at minimum lookahead, and the arcade content is
what puts it there."** 22_scene_load.py prices the scene-change case properly,
clocks included; the mechanical seek is still unmodelled (B1) and is charged on
top of all of it.
### 56.6 What the cross-check IS worth, now that it is not a cross-check
Run anyway, against 518 SNES chapter XMLs, with the 40 scene abbreviations
matched to DirkSimple scenes **from the data** — the abbreviation's letters must
be a subsequence of the scene name, ranked by sequence-name overlap, with a
reversed-scene rule for the thirteen mirrored scenes. (Overlap alone mapped
`snkr` to `black_knight`, because two scenes full of `seqN` names look alike.)
- **Start times do not compare at all.** The SNES XMLs are on their own
extraction's timeline; DirkSimple is on arcade ROM frames minus 6,297 ms. The
difference is not a constant and **does not even keep its sign** (spread
-8.0 s .. +6.4 s). Reported as a spread rather than as a check, because a
check it is not.
- **Durations do compare** — they are offset-invariant. 388/505 agree within one
laserdisc frame (76.8%), median difference 0 ms.
- **Branch structure compares**: 470/505 chapters (93.1%) carry the identical
set of (input -> target) edges. Of the 35 that differ, **16 are renames**
(`captured_by_ghouls` -> `ghoul_capture`) and **18 of the remaining 19 are the
SNES conversion dropping the arcade's diagonals**. The 19th is
`intr_castle_exterior`, where the SNES *added* a skip.
**Zero transcription discrepancies were found, and none could have been.** What
the diff produced is one useful fact about our own input layer, below.
### 56.7 Two constraints on the input layer, from the same import
1. **The arcade uses eight directions plus action and start.** Counted over the
906 windows: left 233, right 217, up 209, down 152, action 72, **upleft 13,
upright 4, downleft 3, downright 1**, start 2. The diagonals are 21 windows
out of 906 — rare enough to be dropped by a port that had to (the SNES one
did) and **not** droppable by one aiming at the arcade.
2. **The shortest input window is 98 ms.** Median 950 ms (11.4 frame slots), p10
393 ms, but the floor is 98 ms — and by 54.4 a 12 fps frame slot is 72.13 ms
or 90.16 ms, never 83.33. **A 98 ms window is one or two frames wide.**
Polling input on the frame tick is therefore marginal by construction: the
input layer has to run off something faster than the frame clock, and
`src/player/clock.i` already owns an MFP interrupt at 181.35 clocks a
V-DISP (54.2) that is 8.6x faster and costs 0.1% of the budget.
### 56.8 What this does NOT establish
1. **No rate here is measured.** Every column is a sensitivity across explicit
rates (FINDINGS 50). B1 is still open and the mechanical seek still has no
figure at all.
2. **The gap model is a lower bound by construction.** It takes the earliest
instant an input window opens, so it is what an *expert* player can force,
not what a typical one produces. That is the right bound for a buffer
design and the wrong one for describing play.
3. **It is the arcade's graph, not this port's.** The 612 transitions assume
the port reproduces every branch. Nothing has been mapped onto our 224
Blu-ray streams yet — the SNES project's 516 chapters are finer-grained than
our streams, and that mapping is still C1's problem.
4. **Nothing ran on the 68000 this session.** This is host-side analysis of an
imported table. `decode.s`, `stream.s`, `ring.i`, `clock.i` and `load.i` are
untouched, `decode.bin` is still 1,296 B at the same MD5, and the green light
was ALL GREEN before and after.
5. **The scene graph is not vendored, and the coupling is contained.** Neither
repo ships here and neither is redistributable from this tree; both are
permissive (DirkSimple zlib, Ryan C. Gordon; SNES project MIT, Chad
Doebelin) and both are cloned by the reader. The `sources` block of every
generated table carries the attribution. **`tmp/scenegraph.json` is
generated, gitignored and derived data**: committing it, or any table built
from it, is redistribution and the attribution has to travel with it.