fix(ocgrid): stop on INT/TERM, refuse a saturated stock cell as baseline

Two review findings, both in the measurement guards the script exists to
enforce.

The trap cleaned up but never stopped. On Ctrl-C the handler ran rm and the
sweep carried on: the signal killed only the current cells frame_hash_ab, that
cell was recorded FAILED, and six more multi-minute cells followed. TERM from
timeout was ignored the same way. Split into an EXIT trap that cleans up and an
INT/TERM trap that exits 130.

A saturated stock cell still became the baseline. The doc is emphatic that
SATURATED means unmeasured and arms must not be compared -- but any
non-saturated cell would still print an authoritative-looking percentage
against a ceiling-pinned number. Only a measured stock cell can set the
baseline now; otherwise every row reports NO USABLE STOCK BASELINE.

Super Burnout hit exactly this case in the same run that prompted the fix:
1799/1800 in all seven cells, stock included.

Also records gate 1 complete across all eight candidates. One passes.

  Cybermorph          -48.7% / +44.7% / +72.8%   PASSES
  I-War               -18.2% / +6.3%  / +1.1%    noise, capped at stock
  Missile Command 3D   -0.2% / -14.3% / -11.2%   overclock actively harmful
  Doom, Club Drive, Iron Soldier                 flat locks
  Super Burnout                                  saturated at stock
  Val D Isere                                    still needs a gameplay save

The structural-null hypothesis holds as the general rule but not as a law:
Cybermorph disproves the strong form. Doom reads 593/1800 = 19.8 fps in
gameplay against real hardwares ~20 renders/s, which is a lead for #401 --
the 2x-too-fast figure came from the attract demo, a different code path.

Refs #378

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Opus 5 committed 2026-08-25 23:38:37 -04:00
1 parent ac7a8e0e3f
commit 2b180cb6d4
2 files changed
+89 -58

No files matched your search

+75 -55
View File
@@ -74,16 +74,28 @@ states measured above were older versions that loaded with only a size warning.
ROMs for all eight candidates are present in `jaguar-roms-private/ROMS/`.
### Measured 2026-08-25
### Measured 2026-08-25 — all eight candidates
| Title | Savestate | Result |
|---|---|---|
| **Cybermorph** | `Cybermorph (1993).state1` | **CLEARS GATE 1** — +44.7% at RISC 1.5x, +72.8% at 2x |
| Iron Soldier | `iron_soldier_v104_f2400.state` | Null — clock-insensitive, confirmed by underclock control |
| Val D'Isere | `... (1994).state.auto` | **UNMEASURED** — near-static scene, needs a real gameplay state |
Gate 1 is **complete**. One title passes.
**Cybermorph is the first title to clear gate 1.** All seven cells, one 2400-frame window
(saturation ceiling 2399):
| Title | control 0.5x | RISC 1.5x | RISC 2x | verdict |
|---|---:|---:|---:|---|
| **Cybermorph** | −48.7% | **+44.7%** | **+72.8%** | **PASSES** |
| I-War | −18.2% | +6.3% | +1.1% | fails — noise, non-monotonic |
| Missile Command 3D | −0.2% | **−14.3%** | −11.2% | fails — overclock makes it *worse* |
| Doom | ±0.0% | ±0.0% | ±0.0% | flat lock, 593/1800 |
| Club Drive | ±0.0% | ±0.0% | ±0.0% | flat lock, 1471/1800 |
| Iron Soldier | −0.9% | ±0.0% | ±0.0% | flat lock, 116/900 |
| Super Burnout | SATURATED | SATURATED | SATURATED | unmeasurable — already at the ceiling |
| Val D'Isere | ±0.0% | ±0.0% | ±0.0% | **unmeasured** — static savestate |
**One pass in eight.** The structural-null hypothesis from #378 survives as the *general* rule —
most Jaguar titles are paced by a field lock or their own frame cap, not by clock speed — but it
is not universal, and Cybermorph is the counterexample that disproves the strong form of it.
#### Cybermorph — PASSES
All seven cells, one 2400-frame window (saturation ceiling 2399):
| cell | transitions | ≈ fps | vs stock |
|---|---:|---:|---:|
@@ -95,61 +107,69 @@ ROMs for all eight candidates are present in `jaguar-roms-private/ROMS/`.
| RISC 1.5x, 68K 1.5x | 1994/2400 | ~50 | +46.7% |
| RISC 2x, 68K 1.5x | 2311/2400 | ~58 | +70.1% |
The 68K axis is inert (+0.2% on its own): this is a RISC-bound title, so only
`risc_clock_scale` matters, and stacking the 68K scale on top adds nothing.
The 68K axis is inert (+0.2% on its own): RISC-bound title, so only `risc_clock_scale` matters,
and stacking the 68K scale on top adds nothing.
**The control is what makes the rest trustworthy.** RISC 0.5x costs 48.7% — near-linear
against the +44.7% that 1.5x gains — so the metric tracks clock speed in both directions on
this title. Every figure above comes from the same 2400-frame window; nothing is spliced
across window lengths.
**The control is what makes the rest trustworthy.** RISC 0.5x costs 48.7% — near-linear against
the +44.7% that 1.5x gains — so the metric tracks clock speed in both directions here. Every
figure comes from the same 2400-frame window; nothing is spliced across window lengths.
At 2x, 2349 of 2400 fields carry a new frame — Cybermorph is now presenting at essentially
the display rate. That is a real ceiling rather than the trap-2 counter artefact, but it does
mean **+72.8% is a floor on the 2x gain, not the exact figure**; the engine may have headroom
the display cannot show.
At 2x, 2349 of 2400 fields carry a new frame: it is presenting at essentially the display rate.
That is a real ceiling rather than the trap-2 counter artefact, but it does mean **+72.8% is a
floor on the 2x gain, not the exact figure**.
> **Two blockers before this becomes a DB preset.** Cybermorph is *also* the title with the
> open overclock crash report — #463, "Codex-level crash + erratic ship movement under
> overclock", currently `blocked` for want of reporter artefacts. The one title measured to
> benefit is the one with an unconfirmed overclock crash against it, so #463 has to be settled
> first. And gate 2 still applies: a title can render more frames while its logic runs too
> fast (#401). Play-test before writing a row.
> **Two blockers before this becomes a DB preset.** Cybermorph is *also* the title with the open
> overclock crash report — #463, "Codex-level crash + erratic ship movement under overclock",
> `blocked` for want of reporter artefacts. The one title measured to benefit is the one with an
> unconfirmed overclock crash against it, so #463 has to be settled first. And gate 2 still
> applies: a title can render more frames while its logic runs too fast (#401).
**Iron Soldier is a genuine null, and the control proves it.** 0.5x reads 115 against 1x's
116 — halving the RISC clock costs it *nothing*, so it was never clock-bound at 1x. That fits
the profile data: Iron Soldier is the extreme DSP case, 67% of frame time in the DSP with
99.7% of that spinning in an idle loop. It needs **idle-skip**, not overclock.
#### Missile Command 3D — fails, and interestingly
> **Always run the underclock arm.** A flat 1x/1.5x/2x row is ambiguous — genuine cap, or dead
> measurement? 0.5x separates them. Cybermorph's control (−48.7% at 0.5x, against +44.7% at
> 1.5x) is what proves the metric was live for the whole sweep. `scripts/ocgrid.sh` runs this
> arm unconditionally so it cannot be forgotten.
The only title where overclocking is actively **harmful**: −14.3% at RISC 1.5x, −11.2% at 2x,
while the 0.5x control is flat (−0.2%). Not compute-bound at stock, and giving the RISC more
cycles *costs* presented frames. Worth a look on its own terms — a title that renders less when
given more cycles suggests a timing-sensitive path, not merely an absent benefit.
**Val D'Isere is not a null — it is unmeasured.** 15 transitions in 900 frames, identical even
at 0.5x. That is one changed frame every ~60, i.e. a static or paused screen; the state is a
`.state.auto` autosave that landed wherever the session ended. Needs a real gameplay save
(see below).
#### I-War — fails, capped at stock
### Need a savestate — checklist
Control drops cleanly (−18.2%), so the sweep is valid, but the gains are noise and
non-monotonic: +6.3% at 1.5x, +1.1% at 2x, −3.7% at 2x with the 68K scale. Underclocking hurts
while overclocking does nothing — the signature of a cap sitting at or just above stock.
- [ ] **Doom** — `Doom - Evil Unleashed (1994).jag`
*Where:* in a level, walking down a corridor with at least one monster active.
**Not** the attract demo (its pace is the #401 subject and it desyncs across cycle
changes, so it is invalid evidence either way) and **not** the menu (its speed is the
game's own code — see #396 notes).
- [ ] **Club Drive** — `Club Drive (1994).jag`
*Where:* mid-race, car moving, scenery scrolling.
**Note:** this title used to abort at `risc_clock_scale=2x` (`dsp_pc_escape`, #565).
That is **fixed** as of v3.5.0, so the 2x arm should now complete — if it dies, that is
a regression worth its own issue.
- [ ] **I-War** — `I-War (1995).jag`
*Where:* in flight with geometry on screen. Earlier runs **saturated** here (600/600),
so favour a calmer stretch and consider `FRAMES=1800`.
- [ ] **Missile Command 3D** — `Missile Command 3D (1995).jag`
*Where:* a wave in progress with incoming missiles.
Earlier runs were corrupted by generic input (halved at 1.5x) — a savestate fixes that.
- [ ] **Super Burnout** — `Super Burnout (1995).jag`
*Where:* mid-race at speed.
#### Doom, Club Drive, Iron Soldier — flat locks
Identical counts in all seven cells, control included. Iron Soldier reads 115 at 0.5x against
116 at 1x, so halving the clock costs it nothing.
A flat control would normally void a sweep, and the script says so. It does not here, because
the harness is demonstrably live on the same binary in the same session: Cybermorph, I-War and
Missile Command 3D all responded. These are genuine caps, not dead measurements.
**Doom's number is worth recording for #401.** 593/1800 is one new frame per 3.03 fields,
**≈19.8 fps** — and real hardware Doom level 1 is ~20 renders/s typical. In *gameplay* we may
be far closer to hardware than the attract demo suggests, where #434 measured us at 2.00
fields/flip against hardware's 4.0. One savestate is not a calibration, and the demo and
gameplay are different code paths, so this is a lead rather than a result.
#### Super Burnout — unmeasurable, and the reason matters
1799/1800 in every cell: every frame differs, so the counter is pinned to the ceiling and a gain
has nowhere to show. Lengthening the window does **not** help — a title that renders a new frame
every field is saturated at any window length.
But the reason it saturates is itself the answer: **stock already presents at the display rate**,
so there is no headroom for an overclock to deliver. Formally unmeasured by this metric;
practically, there is nothing to win.
#### Val D'Isere — the one still outstanding
15 transitions in 900 frames, identical even at 0.5x — one changed frame every ~60, i.e. a
static or paused screen. The state is a `.state.auto` autosave that landed wherever the session
ended.
- [ ] **Val D'Isere** — `Val D'Isere Skiing & Snowboarding (1994).jag`. Needs a real gameplay
save: mid-run down a slope, scenery moving, not the menu or a results screen.
### Already measured out — do not re-run
+14 -3
View File
@@ -30,7 +30,12 @@ STATE=$1; shift
FRAMES=${FRAMES:-900}
TMP=$(mktemp -d)
trap 'command rm -rf "$TMP"' EXIT INT TERM
# EXIT does the cleanup; INT/TERM must additionally *stop*. Without the
# second trap a Ctrl-C killed only the current cell's frame_hash_ab (the
# process group gets the signal), that cell was recorded FAILED, and the
# sweep carried on through six more multi-minute cells.
trap 'command rm -rf "$TMP"' EXIT
trap 'exit 130' INT TERM
emit() { printf '%s\n' "$*" | tee -a "$OUT"; }
@@ -64,7 +69,13 @@ for cell in $cells; do
t=$(awk -F, 'NR>1{if(p!=""&&$6!=p)n++;p=$6}END{print n+0}' "$csv")
n=$(awk 'NR>1' "$csv" | wc -l | tr -d ' ')
printf '%s %s\n' "$t" "$n" > "$TMP/$tag.res"
[ "$cell" = "risc=1x,m68k=1x" ] && base=$t
# Only a MEASURED stock cell may become the baseline. A saturated one
# is pinned to the ceiling, so quoting other arms against it would
# print authoritative-looking percentages against a meaningless
# number -- the exact trap this script exists to make un-missable.
if [ "$cell" = "risc=1x,m68k=1x" ] && [ "$n" -gt 0 ] && [ "$t" -lt "$((n - 1))" ]; then
base=$t
fi
done
# --- phase 2: report --------------------------------------------------------
@@ -82,7 +93,7 @@ for cell in $cells; do
elif [ "$t" -ge "$((n - 1))" ]; then
v="SATURATED -- unmeasured, lengthen the window"
elif [ -z "$base" ] || [ "$base" -eq 0 ]; then
v="no usable stock baseline -- static scene, re-save the state"
v="NO USABLE STOCK BASELINE -- stock cell saturated or static; nothing here is comparable"
else
v="$(awk -v a="$t" -v b="$base" 'BEGIN{printf "%+.1f%% vs stock", (a-b)*100.0/b}')"
[ "$cell" = "risc=0.5x,m68k=1x" ] && v="$v <- CONTROL: must drop, or the sweep is void"