<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Johnny Castaway PS1 — Devlog</title>
  <subtitle>Dated, unedited worklogs from the Johnny Castaway PS1 port. Dead ends preserved.</subtitle>
  <link rel="self" type="application/atom+xml" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/feed.xml"/>
  <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/"/>
  <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/</id>
  <updated>2026-05-03T00:00:00-04:00</updated>
  <generator uri="https://jekyllrb.com/" version="hand-rolled">Jekyll (no plugin)</generator>
  <rights>Code GPL-3.0. Sierra art and the Johnny Castaway character are © Sierra On-Line and not licensed under GPL — see /legal/.</rights>
  <author>
    <name>Hunter Davis</name>
    <uri>https://hunterdavis.com/</uri>
  </author><entry>
    <title>Scene Set lineup expands to seven categories — May 3, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/scene-set-expansion/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/scene-set-expansion/</id>
    <published>2026-05-03T00:00:00-04:00</published>
    <updated>2026-05-03T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Pause menu Scene Set selector grows from two pools to seven; not-yet-validated scenes play today and self-upgrade as scenes get visually signed off.</summary>
    <content type="html"><![CDATA[<p>The Scene Set menu item shipped two days ago with two pools: <em>All Scenes</em>
(the catch-all proven rotation) and <em>Fishing Only</em>. The plan was always
that more sets would follow as scene families validated. After looking
at the actual catalog state, the right move turned out to be the
opposite — ship the whole lineup right now, and let the pools improve
in place as scenes get visually signed off.</p>

<h2 id="whats-in-the-lineup">What’s in the lineup</h2>

<p>Seven sets, live since v0.6.9-ps1 and current as of v0.6.10-ps1:</p>

<table>
  <thead>
    <tr>
      <th>Set</th>
      <th>Pool</th>
      <th>Validation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>All Scenes</td>
      <td>catch-all (proven rotation)</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Fishing Only</td>
      <td><code class="language-plaintext highlighter-rouge">fishing1</code>..<code class="language-plaintext highlighter-rouge">fishing8</code></td>
      <td>all ✅</td>
    </tr>
    <tr>
      <td>Johnny Stories</td>
      <td><code class="language-plaintext highlighter-rouge">johnny1</code>..<code class="language-plaintext highlighter-rouge">johnny6</code></td>
      <td><code class="language-plaintext highlighter-rouge">1</code>..<code class="language-plaintext highlighter-rouge">4</code> ✅, <code class="language-plaintext highlighter-rouge">5</code>/<code class="language-plaintext highlighter-rouge">6</code> playing pre-validation</td>
    </tr>
    <tr>
      <td>Mary Visits</td>
      <td><code class="language-plaintext highlighter-rouge">mary1</code>..<code class="language-plaintext highlighter-rouge">mary5</code></td>
      <td>all ✅ as of v0.6.10</td>
    </tr>
    <tr>
      <td>Visitors</td>
      <td><code class="language-plaintext highlighter-rouge">visitor1</code>, <code class="language-plaintext highlighter-rouge">visitor3</code>..<code class="language-plaintext highlighter-rouge">visitor7</code></td>
      <td>playing pre-validation</td>
    </tr>
    <tr>
      <td>Activities</td>
      <td><code class="language-plaintext highlighter-rouge">activity1</code>, <code class="language-plaintext highlighter-rouge">activity4</code>..<code class="language-plaintext highlighter-rouge">activity12</code></td>
      <td>playing pre-validation</td>
    </tr>
    <tr>
      <td>Misc &amp; Suzy</td>
      <td><code class="language-plaintext highlighter-rouge">suzy1</code>, <code class="language-plaintext highlighter-rouge">suzy2</code>, <code class="language-plaintext highlighter-rouge">miscgag1</code>, <code class="language-plaintext highlighter-rouge">miscgag2</code></td>
      <td>playing pre-validation</td>
    </tr>
  </tbody>
</table>

<p>Two scene names were left out on purpose. <code class="language-plaintext highlighter-rouge">visitor2</code>, <code class="language-plaintext highlighter-rouge">activity2</code>, and
<code class="language-plaintext highlighter-rouge">activity3</code> have no FG2 packs on disc — including them in a pool would
risk a pack-start failure on a random pick — so the pools follow the
shape of the actual asset table. Suzy and Miscgag were combined
because each family alone is only two scenes; combined they’re a
believable rotation.</p>

<h2 id="why-ship-pre-validation">Why ship pre-validation</h2>

<p>Scene Set is a random-rotation pool selector, not a scene viewer. The
runtime cost of a not-yet-validated scene is the same as a validated
one: load the FG2 pack, play it, tick the calendar. The visuals may
not be pixel-perfect, the SFX may need rework, but the scene plays. As
soon as a scene moves from ⏳ to ✅ in the <a href="../scenes/">scene
ledger</a>, it looks right in whichever
pool already contains it.</p>

<p>The alternative was empty pools that fall back to <em>All Scenes</em> until
their family validates. The selector framework supports this — the
fallback already exists at <code class="language-plaintext highlighter-rouge">jc_reborn.c:281</code> — but the menu would have
five rows that all played the same content, which is exactly the
“why are we showing this option” UX problem we built the pool labels
to avoid. Real scenes from day one is the better trade.</p>

<h2 id="how-the-framework-holds-up">How the framework holds up</h2>

<p>The original Scene Set design doc said adding a set should be a
one-line entry in <code class="language-plaintext highlighter-rouge">gSceneSetPools</code> and a one-line entry in
<code class="language-plaintext highlighter-rouge">kSceneSetNames</code>. That held. The expansion patch is 60 lines of
diff: five <code class="language-plaintext highlighter-rouge">kSet&lt;Family&gt;Scenes</code> arrays, five new pool entries, five
new label strings, and an updated table in the pause-menu reference.
No engine changes, no new pipeline, no walk-or-frog-clock surgery
needed.</p>

<p>The on-disc FG2 catalog also held. Every scene named in any pool
already had its pack shipped — there was nothing to bake, no asset
re-work, no <code class="language-plaintext highlighter-rouge">cd_layout.xml</code> edit. That’s the dividend of the FG2 +
pack-start design choice, and it pays off here: the <em>playable</em>
catalog has been ahead of the <em>validated</em> catalog for a while, and
the Scene Set framework is just the first feature that makes that
gap user-visible in a useful way.</p>
]]></content>
  </entry><entry>
    <title>Scene Set framework and an animated frog clock — May 3, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/scene-set-and-frog-clock/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/scene-set-and-frog-clock/</id>
    <published>2026-05-03T00:00:00-04:00</published>
    <updated>2026-05-03T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>PS1 port pause menu adds a Scene Set selector and an animated frog-clock loading transition; sequence-reset on cycle keeps walks intact.</summary>
    <content type="html"><![CDATA[<p>This post is about two small features that ended up tangled together
because the PS1’s heap rules don’t let you treat them as separate
problems.</p>

<h2 id="scene-set-pool-by-category">Scene Set: pool-by-category</h2>

<p>The screensaver loop has always picked from a pool called
<code class="language-plaintext highlighter-rouge">kProvenScenes</code>. As more scenes get validated the pool grows, but the
shape stays the same: one big bag, the loop draws a random scene out
of it, the FG2 pack plays, the loop ticks the calendar, and around it
goes. There was no way to say “I’d like the next half hour to be
fishing scenes only” without forcing a pinned scene from the dev
console — which then plays the same scene every iteration.</p>

<p>Scene Set lifts that into a first-class menu item. The new item sits
right under Resume, defaults to <strong>All Scenes</strong>, and the only other
ship-quality alternative right now is <strong>Fishing Only</strong> — the eight
validated <code class="language-plaintext highlighter-rouge">fishing1..fishing8</code> packs. As more scene families lock in,
they get a one-line entry in <code class="language-plaintext highlighter-rouge">gSceneSetPools</code> and a label in
<code class="language-plaintext highlighter-rouge">kSceneSetNames</code>, no other code changes needed. The picker reads
<code class="language-plaintext highlighter-rouge">pauseMenuSceneSet</code> each iteration and indexes into the pool array.</p>

<p>The interaction model is deliberately different from the rest of the
main menu. Left and Right scroll a <em>pending</em> preview, which the row
shows as <code class="language-plaintext highlighter-rouge">&lt;Fishing Only&gt;</code> with an asterisk if the preview hasn’t been
committed yet. Cross or Start commits. Up or Down (which moves the
cursor off the row) discards the preview. That separation matters
because the alternative — “scrolling lands you in a new pool
immediately” — felt like an accidental click hazard the first time
the row got tested. The brackets are the visual cue that this row
takes horizontal input; everything else still uses Cross-to-select.</p>

<h2 id="frog-clock-as-a-real-animation">Frog clock as a real animation</h2>

<p>The PS1 port has had a “meanwhile” frog-clock loading frame since the
freeplay branch landed. Up to today it was a single static image: load
<code class="language-plaintext highlighter-rouge">MEANWHIL.BMP</code>, draw the card sprite at <code class="language-plaintext highlighter-rouge">(254, 100)</code>, draw one fixed
hand frame at <code class="language-plaintext highlighter-rouge">(287, 141)</code>, and call it done. It looked like a paused
clock, which is exactly the wrong feel for a loading transition.</p>

<p>Replacing the static draw with a hand-cycle was a one-line change in
isolation. Replacing it with a hand-cycle that <em>looked right</em> needed
the original script. <code class="language-plaintext highlighter-rouge">MEANWHIL.TTM</code> carries 16 hand sprites split
between an hour-hand set (sprites 5–16) and a minute-hand set
(sprites 1–4), and the script draws each frame at a sprite-specific
<code class="language-plaintext highlighter-rouge">(x, y)</code> because the bounding box of a rotated hand changes per
angle. Drawing every hand frame at the same top-left position made
the hand visibly drift around the clock face on each frame.</p>

<p>A short Python decoder over the TTM bytecode pulled out the per-sprite
draw positions; they’re now a static <code class="language-plaintext highlighter-rouge">kMeanwhilePos[17][2]</code> table
inside <code class="language-plaintext highlighter-rouge">grShowMeanwhileLoadingFrame</code>. The animation cycles the hour
hand every four frames and the minute hand every frame, for a loop of
36 vblanks (~0.6 s at 60 Hz). On each frame: stamp the card, then the
current hour hand at its baked anchor, then the current minute hand
at its baked anchor.</p>

<h2 id="walk-vs-frog-clock-why-the-cycle-does-a-sequence-reset">Walk vs frog clock: why the cycle does a sequence reset</h2>

<p>The honest part of this post is what happened when those two features
met for the first time.</p>

<p>Cycling Scene Set fires the frog clock as its loading transition. The
frog clock calls <code class="language-plaintext highlighter-rouge">grInitEmptyBackground</code> to give the card a clean
black backdrop — that zeros the four background tiles in RAM. That
was always the case; the change is that the <em>next</em> thing now is a
walk transition (the screensaver loop tries to walk Johnny from his
last spot to the new scene’s start). And the walk subsystem composes
Johnny over those background tiles. Black tiles → black borders
around Johnny.</p>

<p>The first attempt was to snapshot the tiles before the wipe and
restore them after. On desktop that’s a nothing change. On PS1 it’s
600 KB of memcpy across four <code class="language-plaintext highlighter-rouge">bgTile*</code> buffers, and the heap is
already tight enough that the snapshot allocation introduced a
noticeable hitch on the first cycle of the run. Restoring also didn’t
fully solve the problem because the rect-mode clean buffers walk
actually reads from are not the same set as the full-tile clean
buffers <code class="language-plaintext highlighter-rouge">grSaveCleanBgTiles</code> populates.</p>

<p>The fix that actually worked is structural. Cycling Scene Set is a
sequence reset by definition: the user just told the loop to throw
out its remembered context. So
<code class="language-plaintext highlighter-rouge">pauseMenuRequestSceneSetCycle</code> consumers now also clear
<code class="language-plaintext highlighter-rouge">storyCurrentSpot</code> and <code class="language-plaintext highlighter-rouge">storyCurrentHdg</code> and set
<code class="language-plaintext highlighter-rouge">fgLoopSequenceJustReset = 1</code>. With the reset flag set, the existing
guard at the top of the loop skips the walk for that iteration. The
next scene’s <code class="language-plaintext highlighter-rouge">foregroundPilotPlay</code> does its own <code class="language-plaintext highlighter-rouge">grLoadScreen</code> and
pulls the bg back to a known good state before any compositing
happens. No 600 KB snapshot, no walk corruption, no extra moving
pieces.</p>

<p>Walks resume on the very next iteration because the next scene
updates <code class="language-plaintext highlighter-rouge">storyCurrentSpot/Hdg</code> on completion, so the user-visible
behavior is just: change the pool, see the frog clock, see “Scene
Set: Fishing Only” caption, see a clean cut to the next fishing
scene.</p>

<h2 id="what-this-is-not">What this is not</h2>

<p>This release does not add new scene sets beyond Fishing Only.
Johnny-only, Mary-only, and Visitors-only sets are obvious next
steps, but they need their own scenes validated under the Scene Set
flow before they ship. The <code class="language-plaintext highlighter-rouge">gSceneSetPools</code> table is structured so
that adding them is a label and an array literal, not a rewrite.
This release also does not add a separate art asset for the
animation; the frog clock uses Sierra’s existing 17-frame
<code class="language-plaintext highlighter-rouge">MEANWHIL.BMP</code> exactly as shipped.</p>

<p>The PS1 port keeps trying to make every player-visible feature land
on top of the same handful of trusted runtime pieces. A new menu
item and a smarter loading frame are exactly the kind of additions
that should not need new pipelines, and after today they don’t.</p>
]]></content>
  </entry><entry>
    <title>Ocean Ambience — v0.6.0-ps1 — May 1, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/ocean-ambience-v0-6/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/ocean-ambience-v0-6/</id>
    <published>2026-05-01T15:00:00-04:00</published>
    <updated>2026-05-01T15:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>v0.6.0-ps1 ships a CC0 ocean ambience loop on a dedicated SPU voice. Pause-menu toggle, memcard-persisted, zero CPU cost in steady state.</summary>
    <content type="html"><![CDATA[<p>Sierra’s <em>Johnny Castaway</em> never had continuous music. The 1992 PC release
drove a few PC-speaker tones and short Sound-Blaster digital effects but
ran in silence the rest of the time. <code class="language-plaintext highlighter-rouge">v0.6.0-ps1</code> adds an optional
looping ocean track — the sound of the place we’re showing — without
touching the screensaver loop’s per-VBlank cost.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#why-ocean-and-not-music" id="markdown-toc-why-ocean-and-not-music">Why ocean, and not music</a></li>
  <li><a href="#the-mechanism-pre-loaded-adpcm-in-spu-ram" id="markdown-toc-the-mechanism-pre-loaded-adpcm-in-spu-ram">The mechanism: pre-loaded ADPCM in SPU RAM</a></li>
  <li><a href="#the-loop-20-seconds-crossfade-replace-at-the-seam" id="markdown-toc-the-loop-20-seconds-crossfade-replace-at-the-seam">The loop: 20 seconds, crossfade-replace at the seam</a></li>
  <li><a href="#the-toggle-pause-menu-memcard-zero-flicker" id="markdown-toc-the-toggle-pause-menu-memcard-zero-flicker">The toggle: pause-menu, memcard, zero-flicker</a></li>
  <li><a href="#frame-impact-zero-verified" id="markdown-toc-frame-impact-zero-verified">Frame impact: zero, verified</a></li>
  <li><a href="#what-the-headroom-leaves" id="markdown-toc-what-the-headroom-leaves">What the headroom leaves</a></li>
</ul>

</details>

<h2 id="why-ocean-and-not-music">Why ocean, and not music</h2>

<p>The PS1 has three independent audio paths: the SPU (dedicated coprocessor
with 24 voices, 512 KB of its own RAM, and hardware loop flags built into
the ADPCM block format), CD-DA (Red Book audio decoded by the drive), and
CD-XA ADPCM (decoded by the drive too, interleaved with data sectors).
Each has trade-offs; all of them <em>could</em> play music. The reasons we
picked ocean ambience as the v0.6 ship instead of “background music” in
general:</p>

<ul>
  <li><strong>Spectrum.</strong> Ocean recordings live below ~1 kHz. Sony 4-bit ADPCM’s
quantization noise is loudest above ~4 kHz. On ocean material the
compression is essentially transparent at 11.025 kHz mono — the
tradeoff that <em>would</em> hurt a music track is invisible on this
material.</li>
  <li><strong>Loop forgiveness.</strong> Wave patterns are aperiodic. A loop seam on
music with a clear meter is obvious; on ocean it disappears under the
broadband noise. We didn’t need the seamless-loop acrobatics that a
music track would demand.</li>
  <li><strong>Reverb is the recording.</strong> The SPU has a hardware reverb unit. Music
typically benefits from it. Ocean ambience already <em>is</em> the spatial
sound — we don’t apply reverb on top, and we get the SPU RAM the
reverb workspace would have used back.</li>
  <li><strong>Mood matches the screensaver.</strong> A castaway on a tiny island, ocean
playing quietly underneath. That’s the fiction. Anything else would
have been music laid on top of it.</li>
</ul>

<h2 id="the-mechanism-pre-loaded-adpcm-in-spu-ram">The mechanism: pre-loaded ADPCM in SPU RAM</h2>

<p>Of the five PS1 audio paths, exactly one delivers <strong>strict zero CPU per
frame</strong>: pre-load an ADPCM-encoded sample into SPU RAM at boot, set the
loop flags on the first and last data blocks of the sample, key on a
dedicated voice. The SPU plays it forever in hardware. The CPU never
sees another byte of audio work.</p>

<p>Compared to the alternatives:</p>

<ul>
  <li><strong>SPU streaming from CD</strong> would let us fit hours of music in 16 KB of
SPU RAM, but it costs ~10-20 cycles per chunk-finish IRQ plus steady
CD bandwidth competing with the FG2 prefetch the screensaver loop
already uses. Not zero CPU.</li>
  <li><strong>CD-DA (Red Book)</strong> is gorgeous and free of CPU cost but the PS1
drive can’t read data and play CD-DA at the same time. Enabling it
would kill the FG2 prefetch and break scene playback. Wrong fit for
our streaming pipeline.</li>
  <li><strong>XA-ADPCM</strong> interleaves audio with data sectors — hardware decode,
zero CPU, compatible with prefetch. The right answer for “many
tracks”, but it adds <code class="language-plaintext highlighter-rouge">mkpsxiso</code> XA-track authoring complexity for
capability v0.6 doesn’t need.</li>
  <li><strong>Sequenced (SEQ/VAB / MOD)</strong> gives the deepest expression but a
sequencer ticks the CPU 1-3 ms per VBlank. Real per-frame cost.</li>
</ul>

<p>Pre-loaded ADPCM in SPU RAM was the only choice that delivered the
zero-CPU contract without changing the disc authoring pipeline.</p>

<h2 id="the-loop-20-seconds-crossfade-replace-at-the-seam">The loop: 20 seconds, crossfade-replace at the seam</h2>

<p>The ocean recording is from <a href="https://bigsoundbank.com/sea-waves-s0266.html">BigSoundBank sound 0266 — “Sea: Waves”</a>,
CC0 public domain. Moderate waves, swirls, no birds, no breakers. 57
seconds of source material at 44.1 kHz stereo.</p>

<p>The naive plan was a 60-90 second loop with a fade-in/fade-out at the
seam. That was clean but had audibly-quiet silence at the wraparound —
the listener could hear the loop boundary even if the seam was
click-free. We swapped to a 20-second loop with a 1-second equal-power
crossfade hidden at the start of the body:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Source S (continuous waveform):
    ... A A A B B B C C C D D D E E E F F F ...
                |---------- 20 sec body -----------|--- 1s tail ---|

loop[i] = body[i] * sin(πi/2N) + tail[i] * cos(πi/2N)   for i in [0..N)
loop[i] = body[i]                                       for i in [N..]
</code></pre></div></div>

<p>When the SPU loops <code class="language-plaintext highlighter-rouge">loop[end] → loop[0]</code>, the listener hears: end of
body → tail fading out (which is what would naturally have come next in
the source) → body fading in. By the time the crossfade has fully
exposed the body, we’re past the seam. For ocean material this is
functionally invisible — the seam buries itself in broadband noise.</p>

<p>Bonus: a 20-second loop is ~123 KB instead of the original sketch’s
~370 KB. That ~250 KB savings in SPU RAM is enough room for 2-3
additional ambience tracks under the same toggle mechanism if we ever
want them (calmer, stormier, holiday-tagged). The encoding pipeline
(<code class="language-plaintext highlighter-rouge">scratch/ocean-ambience/make_tight_loop.py</code> + <code class="language-plaintext highlighter-rouge">encode_vag_loop.py</code>) is
already general-purpose — drop any mono 16-bit WAV in, get a tight loop
VAG out.</p>

<h2 id="the-toggle-pause-menu-memcard-zero-flicker">The toggle: pause-menu, memcard, zero-flicker</h2>

<p>Pause → Accessibility → <strong>Ocean: ON / OFF</strong>. LEFT, RIGHT, or X on the
row toggles <code class="language-plaintext highlighter-rouge">oceanAmbientEnabled</code> and immediately calls
<code class="language-plaintext highlighter-rouge">oceanAmbientStart()</code> / <code class="language-plaintext highlighter-rouge">oceanAmbientStop()</code>. The SPU voice keys on or
off in one register write — no fade, no clicking, no audible delay.
The Save Settings to Memcard option persists the choice (<code class="language-plaintext highlighter-rouge">MC_VERSION</code>
bumped 2 → 3, with a graceful v2-load fallback that defaults the new
field to ON). On the next boot <code class="language-plaintext highlighter-rouge">soundInit</code> reads the saved value and
either auto-keys the voice or leaves it silent.</p>

<p>The wiring mirrors the existing <code class="language-plaintext highlighter-rouge">soundMuted</code> accessibility toggle —
same accessor pattern, same memcard schema bump, same register-write
toggle. About 200 lines net across five files.</p>

<h2 id="frame-impact-zero-verified">Frame impact: zero, verified</h2>

<p>The screensaver’s <code class="language-plaintext highlighter-rouge">JCPERF2</code> per-scene metrics (<code class="language-plaintext highlighter-rouge">loop_vb</code>,
<code class="language-plaintext highlighter-rouge">blocking_vb</code>, <code class="language-plaintext highlighter-rouge">overrun_vb</code>, <code class="language-plaintext highlighter-rouge">compose_calls</code>, etc.) are identical with
the ambience on or off. The SPU mixes its 24 voices in hardware
regardless of how many are active; an additional voice costs nothing.
The loop wraparound is a single bit-flag check the SPU hardware does
at the end of each ADPCM block; no IRQ, no DMA, no CPU.</p>

<p>The total cost is ~123 KB of SPU RAM + ~30-60 ms of additional boot
time (the extra CD read and DMA upload of <code class="language-plaintext highlighter-rouge">OCEAN.VAG</code> into SPU RAM).
After boot, no impact at all.</p>

<h2 id="what-the-headroom-leaves">What the headroom leaves</h2>

<p>The SPU has 512 KB of RAM. Existing SFX uses ~94 KB. The ocean loop
uses ~123 KB. After both, ~257 KB is still free — enough for 2-3
additional 20-second ambience tracks under the same pause-menu toggle,
switching tracks via a single <code class="language-plaintext highlighter-rouge">SPU_KEY_OFF</code>/<code class="language-plaintext highlighter-rouge">SPU_KEY_ON</code> pair. The
pause-menu toggle could naturally extend from <strong>ON / OFF</strong> to a short
list (<strong>Off / Ocean / Calm / Stormy</strong>) without changing the
underlying mechanism. Nothing in v0.6 forces that direction; the
headroom is just present if we want it.</p>

<p>Alternately, the same SPU RAM budget supports holiday-tagged ambience
(the holiday system already carries per-holiday metadata in
<code class="language-plaintext highlighter-rouge">gHolidays[]</code>; an optional ambience index per holiday would swap loops
at scene boundaries) or short scene-start stings on spare voices.
These are future work; <code class="language-plaintext highlighter-rouge">v0.6.0-ps1</code> ships exactly one CC0 ocean loop
and a binary toggle.</p>
]]></content>
  </entry><entry>
    <title>Scripted pad input, menu screenshots, and testing the UI like a player</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/scripted-pad-menu-harness/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/scripted-pad-menu-harness/</id>
    <published>2026-05-01T00:00:00-04:00</published>
    <updated>2026-05-01T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>How the Johnny Castaway PS1 build&apos;s scripted pad-input harness drives deterministic menu navigation, captures every pause-menu screen, and turns the result into the /help/menu/ player guide.</summary>
    <content type="html"><![CDATA[<p>Freeplay made the pause menu important enough that screenshots are no
longer decoration. The menu is now a piece of the machine: it enters and
exits Freeplay, opens the gag and visitor catalogs, changes the island’s
world state, plays sound effects, edits date and RNG seed, and turns
captions on and off.</p>

<p>So the test harness grew a controller.</p>

<p>The PS1 build now has an opt-in pad-script layer. At build time,
<code class="language-plaintext highlighter-rouge">config/ps1/PADSCRIPT.TXT</code> is embedded beside <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code>. At runtime it
does nothing unless the boot string includes <code class="language-plaintext highlighter-rouge">pad-script</code> or
<code class="language-plaintext highlighter-rouge">pad-script-log</code>. When enabled, it merges scripted button masks into the
same active-high controller value that the real pad uses. Start is still
Start. Cross is still Cross. The menu code does not get a fake API.</p>

<p>That small choice matters. A host-side key injector would test DuckStation
window focus. This tests Johnny.</p>

<p>The first harness script boots a normal foreground scene, waits 30 seconds,
presses Start, walks the major pause-menu screens, and drops <code class="language-plaintext highlighter-rouge">JCPADSHOT</code>
markers into the PS1 TTY log. DuckStation regtest captures frames every few
VBlanks. Each marker is delayed until the target menu has had time to draw,
and the reporter copies the first dumped frame at or after that point before
rewriting the <a href="../help/menu/">menu help guide</a>.</p>

<p>That gives the project a new kind of documentation: generated from the
running disc, checked by the same deterministic tooling we use for
regression work, and useful to a human who just wants to know what the
System menu does.</p>

<p>The loop is deliberately boring:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wait 30s
tap START
tap DOWN
tap DOWN
tap CROSS
shot freeplay-options 30
</code></pre></div></div>

<p>The power is in the boring part. Once a menu path can be written down as
button presses, it can become a test case, a screenshot, and a release
artifact. If a future refactor breaks Circle as Back, the harness gets
stuck. If a sub-screen grows too tall, the screenshot makes it obvious. If
Start stops working on the SPI pad path again, no guide page gets produced.</p>

<p>That is the whole philosophy of this port in miniature: make the PlayStation
do the thing, make it repeatable, and then let the artifact tell the truth.</p>

<p>The full operational reference is now
<a href="../docs/scripted-input/">Scripted input harness</a>.</p>
]]></content>
  </entry><entry>
    <title>Freeplay debug mode — May 1, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/freeplay-debug-mode/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/freeplay-debug-mode/</id>
    <published>2026-05-01T00:00:00-04:00</published>
    <updated>2026-05-01T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Implementation notes for freeplay/debug mode in the Johnny Castaway PS1 port.</summary>
    <content type="html"><![CDATA[<p>Freeplay changed the project from a passive screensaver port into something
you can touch.</p>

<p>That sounds small until you look at what the PS1 runtime had become by this
point. The ordinary scenes are carefully captured foreground streams. They
are deterministic, measurable, and repeatable. Freeplay is none of those
things. It reads the controller every frame, moves Johnny around the island,
lets the player fish, opens debug catalogs, changes the tide and holidays,
and then has to cleanly hand the whole machine back to the normal random
screensaver loop.</p>

<p>The breakthrough was not making the controller move a sprite. The
breakthrough was refusing to make a second engine. Freeplay reuses the same
pieces that made walking safe in <code class="language-plaintext highlighter-rouge">v0.4.20</code>: a known island backdrop, sparse
clean rects, the shared walk renderer, holiday stamping, captions, SPU sound,
and the pause menu. It is live C code, but it stands on the replay runtime
instead of fighting it.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#the-control-scheme-got-smaller" id="markdown-toc-the-control-scheme-got-smaller">The control scheme got smaller</a></li>
  <li><a href="#the-pause-menu-became-a-debug-cockpit" id="markdown-toc-the-pause-menu-became-a-debug-cockpit">The pause menu became a debug cockpit</a></li>
  <li><a href="#the-frog-clock-became-the-transition-contract" id="markdown-toc-the-frog-clock-became-the-transition-contract">The frog clock became the transition contract</a></li>
  <li><a href="#memory-was-the-real-feature" id="markdown-toc-memory-was-the-real-feature">Memory was the real feature</a></li>
  <li><a href="#what-got-signed-off" id="markdown-toc-what-got-signed-off">What got signed off</a></li>
</ul>

</details>

<h2 id="the-control-scheme-got-smaller">The control scheme got smaller</h2>

<p>The early builds tried to use the whole controller. Gags on face buttons.
Summons on face buttons. L1 shortcuts. R1 shortcuts. Context-sensitive Cross.
Combos. It was technically cute and practically awful.</p>

<p>The current version is deliberately simpler:</p>

<ul>
  <li>D-pad or analog walks Johnny.</li>
  <li>L2 slows him down.</li>
  <li>R2 speeds him up.</li>
  <li>Circle fishes.</li>
  <li>Select clears and rebuilds the freeplay screen.</li>
  <li>R1 plus D-pad changes the four world states: day/night, tide, raft, and
holiday.</li>
  <li>Start opens the pause menu.</li>
</ul>

<p>Everything else moved into menu catalogs. That was the right split. The
controller is for the things a player does while looking at Johnny. Catalogs
are for actions that need names, descriptions, source asset filenames, frame
counts, and memory notes.</p>

<h2 id="the-pause-menu-became-a-debug-cockpit">The pause menu became a debug cockpit</h2>

<p>Freeplay Options now owns Gags, Visitors, Controls, and Clear. Gags and
Visitors are selector pages: title, BMP, frame count, rough memory cost,
description, and “X spawn now.”</p>

<p>That sounds like UI polish, but it is also engineering leverage. A failed
visitor load can skip cleanly instead of crashing. A bad frame range can be
identified by name instead of by button folklore. A sound can be tested from
the sound-test menu without needing to stumble into the one original scene
that plays it.</p>

<p>Circle is Back everywhere. That rule matters more than it sounds. A debug
mode that needs a handwritten controller legend every time you open it is
not finished.</p>

<h2 id="the-frog-clock-became-the-transition-contract">The frog clock became the transition contract</h2>

<p>Freeplay has several expensive boundaries:</p>

<ul>
  <li>enter from an arbitrary screensaver scene;</li>
  <li>exit back to the normal loop;</li>
  <li>clear the freeplay screen;</li>
  <li>apply world options from the pause menu.</li>
</ul>

<p>Those boundaries now show the original meanwhile frog clock. It is the
right visual language for Johnny, and it also tells the tester that the
runtime is doing real teardown/rebuild work rather than freezing.</p>

<p>The memory rule is strict: the frog frame uses a temporary <code class="language-plaintext highlighter-rouge">TTtmSlot</code>, draws
one frame, and releases <code class="language-plaintext highlighter-rouge">MEANWHIL.BMP</code> unconditionally. It is a loading
frame, not a permanent resident.</p>

<h2 id="memory-was-the-real-feature">Memory was the real feature</h2>

<p>The freeplay bug reports were exactly the kind of bug a forever-running PS1
program deserves: black pixels left by an overlay, a delayed second entry
that looked like a freeze, and a crash after leaving freeplay once.</p>

<p>The fixes tightened the shape of the mode:</p>

<ul>
  <li>overlays mark their screen-space rows dirty before present, so captions
and banners do not leave stale pixels behind;</li>
  <li>freeplay hard-frees its clean-rect snapshot on exit instead of leaving a
dormant 300+ KB allocation parked in the heap;</li>
  <li>default entry/exit no longer runs largest-heap probes, because probes are
malloc/free cycles and therefore part of the thing being tested;</li>
  <li>heap probes remain behind <code class="language-plaintext highlighter-rouge">freeplay-log</code>, <code class="language-plaintext highlighter-rouge">freeplay-detail</code>, and
<code class="language-plaintext highlighter-rouge">freeplay-debug</code>;</li>
  <li>movement, ticking, and drawing do no allocation in the steady-state frame
loop.</li>
</ul>

<p>That is the bar for this project now. If a feature only works for five
minutes, it does not work.</p>

<h2 id="what-got-signed-off">What got signed off</h2>

<p>The last visual pass was the important one because the remaining bugs were
all “forever program” bugs, not feature-checkbox bugs:</p>

<ul>
  <li>entry from the normal pause menu shows the frog immediately;</li>
  <li>exit from freeplay shows the frog before the screensaver loop resumes;</li>
  <li>second entry no longer has a dead pause that reads as a freeze;</li>
  <li>world options apply immediately in freeplay;</li>
  <li>R1 plus D-pad changes day/night, tide, raft, and holidays immediately;</li>
  <li>Select clears the screen and rebuilds the island;</li>
  <li>waves run at the same visual cadence as the screensaver path;</li>
  <li>the frame loop stays allocation-free unless telemetry is explicitly enabled.</li>
</ul>

<p>That is the <code class="language-plaintext highlighter-rouge">v0.5.0-ps1</code> milestone: the first version where Johnny
Castaway on PS1 is not just watching the island, but walking around on it.</p>
]]></content>
  </entry><entry>
    <title>Milestones — April 25, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/milestones-2026-04-25/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/milestones-2026-04-25/</id>
    <published>2026-04-25T00:00:00-04:00</published>
    <updated>2026-04-25T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Status snapshot from April 25, 2026: working TTY, perf module, pause menu, SPI controller driver, holiday expansion design, and memcard support.</summary>
    <content type="html"><![CDATA[<p>A single-day batch that pushed the PS1 port across several long-standing
unknowns: working TTY, an instrumentable perf module wired into a Docker
regtest harness, a real on-PS1 pause menu, a holiday-coverage design
expansion from 4 to 35, a hand-rolled SPI controller driver replacing the
broken BIOS pad path, and the start of memory-card-backed user settings.
What follows is the day’s archeology in bullets, with links to the
locked-in design docs.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#1-working-ps1-printf-to-tty" id="markdown-toc-1-working-ps1-printf-to-tty">1. Working PS1 <code class="language-plaintext highlighter-rouge">printf()</code> to TTY</a></li>
  <li><a href="#2-performance-baseline--docker-regtest-pipeline" id="markdown-toc-2-performance-baseline--docker-regtest-pipeline">2. Performance baseline + Docker regtest pipeline</a></li>
  <li><a href="#3-pause-menu--first-real-user-facing-ui-shipped" id="markdown-toc-3-pause-menu--first-real-user-facing-ui-shipped">3. Pause menu — first real user-facing UI (SHIPPED)</a></li>
  <li><a href="#4-holiday-expansion--4--35-design" id="markdown-toc-4-holiday-expansion--4--35-design">4. Holiday expansion — 4 → 35 design</a></li>
  <li><a href="#5-spi-direct-controller-polling" id="markdown-toc-5-spi-direct-controller-polling">5. SPI direct controller polling</a></li>
  <li><a href="#6-memcard-support-shipped" id="markdown-toc-6-memcard-support-shipped">6. Memcard support (SHIPPED)</a></li>
  <li><a href="#7-tty-log-prefixes" id="markdown-toc-7-tty-log-prefixes">7. TTY log prefixes</a></li>
  <li><a href="#pointers" id="markdown-toc-pointers">Pointers</a></li>
</ul>

</details>

<h2 id="1-working-ps1-printf-to-tty">1. Working PS1 <code class="language-plaintext highlighter-rouge">printf()</code> to TTY</h2>

<ul>
  <li>PSn00bSDK + DuckStation now reliably emits <code class="language-plaintext highlighter-rouge">printf()</code> output to TTY logs.
Earlier in the project this had been intermittent / unreliable — the same
call would surface in some build/runtime configurations and silently drop
in others.</li>
  <li>This is the single largest unblocker for the rest of today’s work.
Instrument-based perf debugging, JCPERF/JCPERF2 line mining, and
one-shot snapshots like <code class="language-plaintext highlighter-rouge">JCPAUSE</code> are all only useful once printf is
trustworthy.</li>
  <li>See <code class="language-plaintext highlighter-rouge">docs/ps1/current-status.md</code> “Known limitations” for the older
caveat (gated probes only); that paragraph is now stale and is being
updated alongside this milestone.</li>
</ul>

<h2 id="2-performance-baseline--docker-regtest-pipeline">2. Performance baseline + Docker regtest pipeline</h2>

<ul>
  <li>New <code class="language-plaintext highlighter-rouge">src/ps1_perf.c/h</code> module landed with a level-gated logging API:
<code class="language-plaintext highlighter-rouge">ps1PerfSetLevel(level)</code> switches between OFF / SUMMARY / DETAIL /
DEBUG. Boot-token aliases (<code class="language-plaintext highlighter-rouge">perf</code>, <code class="language-plaintext highlighter-rouge">perf-detail</code>, <code class="language-plaintext highlighter-rouge">perf-debug</code>) are
parsed by the existing BOOTMODE.TXT path so an external operator can
pick a level without recompiling.</li>
  <li>TTY emits structured <code class="language-plaintext highlighter-rouge">JCPERF</code> / <code class="language-plaintext highlighter-rouge">JCPERF2</code> lines per scene (loop_vb,
target_vb, blocking_vb, hidden_vb, due_misses, render/held entry
counts, CD bytes, restore/upload bytes, etc.). These lines are
designed to be mined by an external agent — every field is a stable,
greppable key=value pair.</li>
  <li><code class="language-plaintext highlighter-rouge">scripts/regtest-*.sh</code> ride on top of the existing DuckStation-headless
Docker image. Frames dump on a fixed interval, TTY captures to stdout,
state hashes capture at exit. Combined, this gives a single command
that produces both visual diff inputs and machine-readable perf
baselines for a regression run.</li>
  <li>See the <a href="../source/docs/ps1/performance-optimization-plan/">performance optimization plan</a>
for the optimization backlog being driven by these numbers.</li>
</ul>

<h2 id="3-pause-menu--first-real-user-facing-ui-shipped">3. Pause menu — first real user-facing UI (SHIPPED)</h2>

<ul>
  <li>Pressing <strong>Start</strong> now opens an overlay during scene playback. Items:
Resume / Sound / Day-Night override (AUTO/DAY/NIGHT) / Holiday override
(AUTO/NONE/HALLOWEEN/ST PATRICK/CHRISTMAS/NEW YEAR) / Save Settings to
Memcard / Reset Screensaver Loop / Next Scene / TTY Perf Log
(OFF/SUMMARY/DETAIL/DEBUG) / Debug Info / Set Time-Date.</li>
  <li><strong>Custom font path (shipped)</strong>: embedded 8x8 ASCII glyph table
(<code class="language-plaintext highlighter-rouge">pmFontBits[96][8]</code> in <code class="language-plaintext highlighter-rouge">src/pause_menu.c</code>), pre-doubled to 16x16 at
upload time (<code class="language-plaintext highlighter-rouge">pmUploadFont</code>), drawn glyph-by-glyph via manual SPRT
primitives (<code class="language-plaintext highlighter-rouge">pmTextDrawChar</code>). PSn00bSDK’s <code class="language-plaintext highlighter-rouge">FntFlush</code> was empirically
broken in the scene-runtime context — it accumulates primitives
without producing any visible pixels — so the in-tree font path
replaces it entirely.</li>
  <li><strong>Panel rendering (shipped)</strong>: translucent dim quad (POLY_F4
semi-trans black) under an opaque purple panel with 12-pixel chamfered
corner cutouts (3-rect rounded-rectangle look), drawn over the live
scene every paused frame.</li>
  <li><strong>Sound mute (shipped)</strong>: <code class="language-plaintext highlighter-rouge">SpuSetCommonMasterVolume</code> is not honored by
DuckStation HLE, so the mute path zeros every voice volume + CD vol +
master vol and clears the master-enable bit in <code class="language-plaintext highlighter-rouge">SPU_CTRL</code> via direct
register writes.</li>
  <li><strong>Day/Night and Holiday overrides (shipped)</strong>: flow into
<code class="language-plaintext highlighter-rouge">fgLoopApplyVariant</code> in <code class="language-plaintext highlighter-rouge">src/jc_reborn.c</code> with priority
<code class="language-plaintext highlighter-rouge">explicit menu override &gt; Set Time/Date soft override &gt; random</code>.</li>
  <li>Files: <code class="language-plaintext highlighter-rouge">src/pause_menu.c/h</code>. Design: <a href="../source/docs/ps1/pause-menu-design/">pause menu design</a>.</li>
</ul>

<h2 id="4-holiday-expansion--4--35-design">4. Holiday expansion — 4 → 35 design</h2>

<ul>
  <li>The existing four holidays (Halloween, St Patrick’s, Christmas, New
Year) covered fewer than half of the calendar months. The new design
doc proposes 35 US-centric holidays with at least one per month and
an average of about three.</li>
  <li>Mix is federal + pop-culture + seasonal (e.g. Pi Day, Talk Like a
Pirate Day, Star Wars Day, Moon Landing Day) so the screensaver
always feels seasonally alive rather than dead-stretches between the
four legacy events.</li>
  <li>Design: <a href="../source/docs/ps1/holidays-expansion-design/">holiday expansion design</a>.
Implementation is not in this batch.</li>
</ul>

<h2 id="5-spi-direct-controller-polling">5. SPI direct controller polling</h2>

<ul>
  <li>The BIOS pad system (<code class="language-plaintext highlighter-rouge">InitPAD</code> / <code class="language-plaintext highlighter-rouge">StartPAD</code>) was empirically broken in
our PSn00bSDK 0.24 + DuckStation environment. Replaced with PSn00bSDK’s
reference SPI driver — timer-2 + SIO0 IRQ-driven, polling at 250 Hz.</li>
  <li>One critical bug found while porting: spicyjpeg’s reference uses
<code class="language-plaintext highlighter-rouge">tx_len=4</code>, but DuckStation’s controller emulation only delivers
button bytes when the <strong>full</strong> 5-byte poll sequence comes from the
TX buffer (<code class="language-plaintext highlighter-rouge">tx_len=5</code>). That single off-by-one is what surfaced as
“Start does nothing” during early pause-menu wiring.</li>
  <li>Files: <code class="language-plaintext highlighter-rouge">src/spi.c/h</code>, integrated into <code class="language-plaintext highlighter-rouge">src/events_ps1.c</code>. The pause
menu, day-night override, and any future input feature all sit on top
of this driver.</li>
</ul>

<h2 id="6-memcard-support-shipped">6. Memcard support (SHIPPED)</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/memcard.c/h</code> saves user pause-menu settings (sound mute,
day/night override, holiday override, soft time/date) to a single 8 KB
block on memcard slot 1, file name <code class="language-plaintext highlighter-rouge">BASLUS-99999JCREB</code>.</li>
  <li><strong>Avoids the BIOS card driver entirely</strong> (<code class="language-plaintext highlighter-rouge">InitCARD</code> / <code class="language-plaintext highlighter-rouge">StartCARD</code> /
<code class="language-plaintext highlighter-rouge">_bu_init</code> / file API). The BIOS driver installs a persistent VBlank
handler that fights our 250 Hz SPI controller-polling timer and tanks
scene framerate. Instead, memcard I/O rides on top of our existing
SPI driver via <code class="language-plaintext highlighter-rouge">SPI_CreateRequest</code> queue + raw byte sequencing per
the nocash memcard protocol.</li>
  <li>During memcard ops the SPI poll rate drops 250 Hz → 50 Hz: a single
card transaction is ~5.5 ms, longer than the 4 ms timer-2 tick that
drives the controller poller, so we widen the tick to give the card
exclusive SPI bus time.</li>
  <li>Save block carries a proper PS1 save header (SC magic + title + icon
frame count + 16-color CLUT) with a hand-drawn 16x16 palm-tree icon,
so the save shows up in the BIOS Memory Card Manager like a normal
game save.</li>
  <li>Boot-time auto-load: if a save exists, settings are restored before
the scene loop starts. The Save Settings menu entry writes; reads
happen automatically at boot.</li>
</ul>

<h2 id="7-tty-log-prefixes">7. TTY log prefixes</h2>

<ul>
  <li>The complete set of greppable log-line prefixes after this batch:
<code class="language-plaintext highlighter-rouge">JCSPI</code> (controller driver), <code class="language-plaintext highlighter-rouge">JCPAD</code> (pad layer), <code class="language-plaintext highlighter-rouge">JCBOOT</code> (boot /
bootmode), <code class="language-plaintext highlighter-rouge">JCPERF</code> / <code class="language-plaintext highlighter-rouge">JCPERF2</code> (perf module), <code class="language-plaintext highlighter-rouge">JCPAUSE</code> (pause-menu
one-shot snapshot), <code class="language-plaintext highlighter-rouge">JCMC</code> (memcard). All are stable key=value pairs
intended for external perf-debugging agents.</li>
</ul>

<hr />

<h2 id="pointers">Pointers</h2>

<ul>
  <li>Design backbone: <a href="../source/docs/ps1/pause-menu-design/">pause menu design</a>,
<a href="../source/docs/ps1/holidays-expansion-design/">holiday expansion design</a>,
<a href="../source/docs/ps1/performance-optimization-plan/">performance optimization plan</a></li>
  <li>Status frame: <a href="../source/docs/ps1/current-status/">current status</a>,
<a href="../source/docs/ps1/scene-status/">scene status</a></li>
  <li>Workflow: <a href="../source/docs/ps1/development-workflow/">development workflow</a></li>
</ul>
]]></content>
  </entry><entry>
    <title>PS1 Fishing 1 Water Animation — Session Worklog, April 21, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/water-animation-worklog/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/water-animation-worklog/</id>
    <published>2026-04-21T00:00:00-04:00</published>
    <updated>2026-04-21T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Session worklog tracing the fishing1 missing-waves bug to an empty background sprite slot after adsInitIsland.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#outcome-so-far" id="markdown-toc-outcome-so-far">Outcome so far</a></li>
  <li><a href="#evidence-gathered" id="markdown-toc-evidence-gathered">Evidence gathered</a></li>
  <li><a href="#what-the-probe-grid-currently-tests" id="markdown-toc-what-the-probe-grid-currently-tests">What the probe grid currently tests</a></li>
  <li><a href="#whats-been-tried-that-did-not-work-this-session" id="markdown-toc-whats-been-tried-that-did-not-work-this-session">What’s been tried that did NOT work (this session)</a></li>
  <li><a href="#recommended-next-steps" id="markdown-toc-recommended-next-steps">Recommended next steps</a></li>
  <li><a href="#fix-candidates-that-are-still-on-the-table" id="markdown-toc-fix-candidates-that-are-still-on-the-table">Fix candidates that are still on the table</a></li>
  <li><a href="#probe-infrastructure-already-in-place-keep" id="markdown-toc-probe-infrastructure-already-in-place-keep">Probe infrastructure already in place (keep)</a></li>
  <li><a href="#boot-hang-root-cause-solved" id="markdown-toc-boot-hang-root-cause-solved">Boot-hang root cause (solved)</a></li>
  <li><a href="#debugging-lessons-learned" id="markdown-toc-debugging-lessons-learned">Debugging lessons learned</a></li>
  <li><a href="#resolution-landed-this-session" id="markdown-toc-resolution-landed-this-session">Resolution (landed this session)</a>    <ul>
      <li><a href="#1-pre-load-backgrndbmp-before-bg-tile-allocation" id="markdown-toc-1-pre-load-backgrndbmp-before-bg-tile-allocation">1. Pre-load BACKGRND.BMP before bg-tile allocation</a></li>
      <li><a href="#2-wave-tick-parity-with-the-normal-ads-path" id="markdown-toc-2-wave-tick-parity-with-the-normal-ads-path">2. Wave-tick parity with the normal ads path</a></li>
      <li><a href="#3-rect-based-clean-backup-option-b" id="markdown-toc-3-rect-based-clean-backup-option-b">3. Rect-based clean backup (option B)</a></li>
      <li><a href="#div-by-zero-guard" id="markdown-toc-div-by-zero-guard">Div-by-zero guard</a></li>
    </ul>
  </li>
  <li><a href="#open-follow-ups-for-subsequent-sessions" id="markdown-toc-open-follow-ups-for-subsequent-sessions">Open follow-ups for subsequent sessions</a></li>
</ul>

</details>

<h2 id="outcome-so-far">Outcome so far</h2>

<ul>
  <li>Root cause of “no waves” found: <strong><code class="language-plaintext highlighter-rouge">ttmBackgroundSlot.numSprites[0] == 0</code></strong>
after <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> returns. Every downstream wave draw bails at the
<code class="language-plaintext highlighter-rouge">numSprites==0</code> check in <code class="language-plaintext highlighter-rouge">grDrawSprite</code>.</li>
  <li>Root cause of the <em>empty slot</em>: <code class="language-plaintext highlighter-rouge">grLoadBmp(&amp;ttmBackgroundSlot, 0,
"BACKGRND.BMP")</code> inside <code class="language-plaintext highlighter-rouge">islandInit</code> cannot allocate enough contiguous heap
for either (a) the ~93 KB <code class="language-plaintext highlighter-rouge">BACKGRND.PSB</code> stream read or (b) the ~150 KB
raw <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> fallback. Slot is never populated.</li>
  <li>The BMP/PSB files ARE on the CD. <code class="language-plaintext highlighter-rouge">ps1PilotActivePack.entries</code> is NULL on
this path (fgpilot, pre-scene), so the “pack is authoritative, skip CD
fallback” early-exit is NOT firing. The failure is purely heap pressure.</li>
  <li>The other fishing1-path BMPs (TRUNK, MRAFT) are ~10 KB and load fine;
BACKGRND is ~10× bigger and is the only one hitting the heap ceiling at
that moment in the scene setup.</li>
</ul>

<h2 id="evidence-gathered">Evidence gathered</h2>

<p>Ran a progressively larger on-screen probe grid (described in detail below).
Key findings verified on DuckStation:</p>

<ul>
  <li><strong>Heap ceiling</strong>: <code class="language-plaintext highlighter-rouge">malloc</code> succeeds for 4K/32K/64K/93K, fails for 150K.</li>
  <li><strong>Stream-read ceiling</strong>: <code class="language-plaintext highlighter-rouge">ps1_streamRead("PSB\\BACKGRND.PSB", N)</code> succeeds
for N=4K/32K, fails for N=64K/93K.</li>
  <li>The stream ceiling is <em>lower</em> than the raw-malloc ceiling because
<code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile</code> allocates <strong>twice</strong>: a sector-aligned staging
buffer AND a size-exact output copy → peak ≈ 2× requested.</li>
  <li><code class="language-plaintext highlighter-rouge">findBmpResource("BACKGRND.BMP")</code> returns non-NULL, has numImages&gt;0 and
uncompressedSize&gt;0, but <code class="language-plaintext highlighter-rouge">uncompressedData</code> stays NULL after load (status
code 4 = “no BMP bytes after load”), consistent with the stream/malloc
pair failing.</li>
</ul>

<h2 id="what-the-probe-grid-currently-tests">What the probe grid currently tests</h2>

<p>A compact row of 10 colored squares at screen y=172 (green = yes/pass,
blue = no/fail):</p>

<ul>
  <li>T27 <code class="language-plaintext highlighter-rouge">malloc(4 KB)</code> — heap baseline</li>
  <li>T28 <code class="language-plaintext highlighter-rouge">malloc(32 KB)</code></li>
  <li>T29 <code class="language-plaintext highlighter-rouge">malloc(64 KB)</code></li>
  <li>T30 <code class="language-plaintext highlighter-rouge">malloc(93 KB)</code> ← PSB file size</li>
  <li>T31 <code class="language-plaintext highlighter-rouge">malloc(150 KB)</code> ← raw BMP file size</li>
  <li>T32 <code class="language-plaintext highlighter-rouge">ps1_streamRead("PSB\\BACKGRND.PSB", 4 KB)</code> — stream baseline</li>
  <li>T33 <code class="language-plaintext highlighter-rouge">ps1_streamRead(…, 32 KB)</code></li>
  <li>T34 <code class="language-plaintext highlighter-rouge">ps1_streamRead(…, 64 KB)</code></li>
  <li>T35 <code class="language-plaintext highlighter-rouge">ps1_streamRead(…, 93 KB)</code> ← exact PSB full read</li>
  <li>T36 <code class="language-plaintext highlighter-rouge">ttmBackgroundSlot.numSprites[0] &gt; 0</code> ← end-to-end success signal</li>
</ul>

<p>If T30 green + T34/T35 blue + T36 blue: “heap OK for a 93 KB lump, but the
2×-alloc stream read can’t fit”. That’s the fix target.</p>

<p>If the stream path is reduced to a 1× alloc peak (either via <code class="language-plaintext highlighter-rouge">memmove</code> or
read-direct-into-target), T34/T35 should flip green and T36 should follow.</p>

<h2 id="whats-been-tried-that-did-not-work-this-session">What’s been tried that did NOT work (this session)</h2>

<p>All three attempts below hung on the title screen — they never got to the
fishing1 scene. The fix must come later, carefully.</p>

<ol>
  <li>
    <p><strong>Load BACKGRND.BMP before <code class="language-plaintext highlighter-rouge">grLoadScreen(OCEAN)</code> in <code class="language-plaintext highlighter-rouge">islandInit</code></strong>
(with MRAFT shuffled to slot 1 so it wouldn’t clobber slot 0). Hung on
title. Most likely <code class="language-plaintext highlighter-rouge">grLoadScreen</code> then failed because the 93 KB PSB held
the big heap lump.</p>
  </li>
  <li>
    <p><strong>Rewrite <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile</code> to split head / bulk / tail sectors
and read directly into the output buffer</strong>. First attempt had a
buffer-overflow bug (bulk chunk writing past <code class="language-plaintext highlighter-rouge">size</code> for non-sector-aligned
reads); second attempt was functionally correct in paper trace but <em>still</em>
hung on title. Probable cause: doing multiple short CdControl+CdRead
cycles for the head/tail splice changes PS1 CD controller state enough
that something early in boot (likely in <code class="language-plaintext highlighter-rouge">parseResourceFiles</code> or
similar very early path) hangs.</p>
  </li>
  <li>
    <p><strong>Simpler rewrite: keep the original read loop but skip the second
<code class="language-plaintext highlighter-rouge">malloc</code> and return the sector buffer after an in-place <code class="language-plaintext highlighter-rouge">memmove</code></strong>.
Also hung on title. Unclear why — memmove semantics should be identical
to the old read-then-memcpy path.</p>
  </li>
</ol>

<p>Given attempts 2 and 3 broke the title boot but were logically equivalent
to the original for <code class="language-plaintext highlighter-rouge">offset==0</code> reads (which is what the title path uses),
something unrelated to streaming is being affected by our build changes.
Could also be an incidental BSS / code-size issue flipping something about
how the PS1 loader lays out the binary.</p>

<h2 id="recommended-next-steps">Recommended next steps</h2>

<ul>
  <li>Revert <code class="language-plaintext highlighter-rouge">cdrom_ps1.c</code> to its last clean state. Keep only the probe row
(T27-T36) + supporting accessors.</li>
  <li>Rebuild. Confirm title boot works and fishing1 scene reaches compose
(even without waves, as the probe row proves we’ve seen before).</li>
  <li>Only then attempt the streaming / memory fix. Before each new attempt,
run <em>just that change</em> and confirm boot still works — do not stack
multiple changes into a single test.</li>
</ul>

<h2 id="fix-candidates-that-are-still-on-the-table">Fix candidates that are still on the table</h2>

<ol>
  <li><strong>Free the OCEAN0X.SCR clean tiles before the BACKGRND load</strong> — <code class="language-plaintext highlighter-rouge">grLoadScreen</code>
keeps ~600 KB of bgTile + clean-tile memory live. If we could free those
temporarily, the 93 KB PSB stream’s 2× peak would fit.</li>
  <li><strong>Pre-load BACKGRND.BMP earlier, <em>before</em> <code class="language-plaintext highlighter-rouge">grLoadScreen</code> runs</strong> — but
without breaking the OCEAN SCR load that immediately follows.</li>
  <li><strong>Use the <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFileIntoBuffered</code> variant</strong> — caller-provided
sector buffer, no malloc in the inner path. Requires plumbing through
<code class="language-plaintext highlighter-rouge">grTryLoadPsb</code> / <code class="language-plaintext highlighter-rouge">grLoadBmpRAM</code>.</li>
  <li><strong>Skip <code class="language-plaintext highlighter-rouge">grLoadScreen(OCEAN)</code> altogether in the fgpilot path</strong> — compose the
ocean base from a smaller source (or use <code class="language-plaintext highlighter-rouge">ISLETEMP.SCR</code> which already has
the ocean baked in). Then we don’t fight the OCEAN SCR for heap.</li>
  <li><strong>Shrink one of the other persistent large allocations</strong> — e.g. the
title screen buffer if still held, or reduce the number of clean bgTile
copies.</li>
</ol>

<h2 id="probe-infrastructure-already-in-place-keep">Probe infrastructure already in place (keep)</h2>

<p><code class="language-plaintext highlighter-rouge">ads.c</code> exposes accessors used by the probe row:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">adsDbgAlloc4K/32K/64K/93K/150KOk()</code></li>
  <li><code class="language-plaintext highlighter-rouge">adsDbgStream4K/32K/64K/93KOk()</code></li>
  <li><code class="language-plaintext highlighter-rouge">adsDbgBackgroundSlotSpriteCount()</code></li>
</ul>

<p>And the one-shot <code class="language-plaintext highlighter-rouge">adsDbgRunProbesOnce()</code> runs on the first compose frame
(cached thereafter), so the probes don’t thrash per-frame.</p>

<h2 id="boot-hang-root-cause-solved">Boot-hang root cause (solved)</h2>

<p>While isolating “HEAD + FISHING.ADS name fix” from everything else, we
proved the title-to-scene hang is caused by <strong>integer division by zero in
<code class="language-plaintext highlighter-rouge">islandAnimate</code></strong> when <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> fails to load.</p>

<p><code class="language-plaintext highlighter-rouge">island.c</code>’s <code class="language-plaintext highlighter-rouge">islandAnimate</code> does:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">grDrawSprite</span><span class="p">(</span><span class="n">grBackgroundSfc</span><span class="p">,</span> <span class="n">ttmSlot</span><span class="p">,</span> <span class="n">waveX</span><span class="p">,</span> <span class="n">waveY</span><span class="p">,</span> <span class="n">waveSpriteNo</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="n">uint16</span> <span class="n">actualSpriteNo</span> <span class="o">=</span> <span class="n">waveSpriteNo</span> <span class="o">%</span> <span class="n">ttmSlot</span><span class="o">-&gt;</span><span class="n">numSprites</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>  <span class="c1">// &lt;- hang</span>
</code></pre></div></div>

<p>Flow:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">fgPlayOceanRuntimeScene</code> calls <code class="language-plaintext highlighter-rouge">adsInitIsland</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">islandInit</code> calls <code class="language-plaintext highlighter-rouge">grLoadBmp(&amp;ttmBackgroundSlot, 0, "BACKGRND.BMP")</code>.
Under the memory conditions present at this point in the fgpilot path,
that load silently fails — the PSB path’s 2× stream alloc (93 KB + 93 KB)
can’t find a free block, and the BMP fallback’s ~150 KB alloc also fails.
<code class="language-plaintext highlighter-rouge">ttmBackgroundSlot.numSprites[0]</code> ends up 0.</li>
  <li><code class="language-plaintext highlighter-rouge">islandInit</code> then runs the initial-wave loop:
<code class="language-plaintext highlighter-rouge">for (int i=0; i &lt; 4; i++) islandAnimate(ttmThread);</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">islandAnimate</code>’s <code class="language-plaintext highlighter-rouge">grDrawSprite</code> call is a silent no-op (the
<code class="language-plaintext highlighter-rouge">imageNo/numSprites</code> guard inside <code class="language-plaintext highlighter-rouge">grDrawSprite</code> returns cleanly).</li>
  <li>The very next line computes
<code class="language-plaintext highlighter-rouge">waveSpriteNo % ttmSlot-&gt;numSprites[0]</code> — an integer <code class="language-plaintext highlighter-rouge">%</code> with divisor 0
on MIPS R3000 traps via the overflow/exception handler, which on this
build effectively deadlocks the console (no handler progresses past it).</li>
  <li>The main loop never starts, the title RAW framebuffer is never
overwritten, so the user sees “stuck on title”.</li>
</ol>

<p><strong>Why it looked like a hang and not a crash:</strong> DuckStation keeps showing
the existing framebuffer (title) if the guest CPU is stuck. From the
outside it’s indistinguishable from a “600-second delay”.</p>

<p><strong>Fix</strong> (in-session): add an early return guard in <code class="language-plaintext highlighter-rouge">islandAnimate</code> before
the modulus operation:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">ttmSlot</span><span class="o">-&gt;</span><span class="n">numSprites</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">return</span><span class="p">;</span>
<span class="n">uint16</span> <span class="n">actualSpriteNo</span> <span class="o">=</span> <span class="n">waveSpriteNo</span> <span class="o">%</span> <span class="n">ttmSlot</span><span class="o">-&gt;</span><span class="n">numSprites</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</code></pre></div></div>

<p>This is defensive — it also protects any other path where the wave slot
could be empty (e.g. a future scene pack change, or a CD read stall).</p>

<h2 id="debugging-lessons-learned">Debugging lessons learned</h2>

<ul>
  <li>On PS1 / this harness, <strong>“stuck on title” is always worth suspecting a
guest CPU fault</strong>, not an I/O or logic wait. Division, modulus, null
deref, and misaligned-load faults all end the same way from a host
observer’s perspective.</li>
  <li>Add div-by-zero / null-slot guards around every <code class="language-plaintext highlighter-rouge">%</code> and array index
whose divisor/index comes from <code class="language-plaintext highlighter-rouge">ttmSlot-&gt;numSprites[...]</code>. There are
several of these in <code class="language-plaintext highlighter-rouge">island.c</code>, <code class="language-plaintext highlighter-rouge">graphics_ps1.c</code> (replay path), and
potentially more — audit on a quiet day.</li>
  <li>When deciding whether a title stall is code vs. a deliberate I/O
backoff, the fastest disambiguator is an on-screen probe row that
updates every frame in <code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeCompose</code>. If the row
never appears, the compose loop never ran — which means either
<code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeStart</code> bailed, or the CPU faulted before we got
there. Worth keeping a minimal probe row as standard debug
scaffolding.</li>
  <li>A cheap way to confirm a CPU fault vs. a real hang in DuckStation: the
console’s own CPU-exception handler is typically quiet on PS1 titles;
but you can open DuckStation’s CPU debugger view (if available) or,
more reliably, check whether the background audio / spinner continues
ticking. Silent audio = CPU probably faulted.</li>
</ul>

<h2 id="resolution-landed-this-session">Resolution (landed this session)</h2>

<p>Fishing 1 now plays through the full scene with three shoreline waves
animating in parallel. The fix is a clean three-part change:</p>

<h3 id="1-pre-load-backgrndbmp-before-bg-tile-allocation">1. Pre-load BACKGRND.BMP before bg-tile allocation</h3>

<p>Added <code class="language-plaintext highlighter-rouge">adsPilotPreloadBackgrndBmp()</code> in <code class="language-plaintext highlighter-rouge">ads.c</code>, called at the very top
of <code class="language-plaintext highlighter-rouge">fgPlayOceanRuntimeScene</code> — <em>before</em> <code class="language-plaintext highlighter-rouge">fgInitVisiblePipeline</code> runs and
allocates the 614 KB of empty bg tiles. With a fresh heap at that moment
the ~93 KB PSB (which peaks at ~186 KB during ps1_streamRead’s 2× alloc)
loads cleanly.</p>

<p>Verified with pre-load heap-tier probes: all 4 KB / 32 KB / 64 KB / 93 KB
/ 150 KB / 200 KB allocs succeed at that moment. The naive placement
(after scene setup) fails every tier above 64 KB.</p>

<h3 id="2-wave-tick-parity-with-the-normal-ads-path">2. Wave-tick parity with the normal ads path</h3>

<p>Added <code class="language-plaintext highlighter-rouge">adsPilotTickBackgroundWaves()</code> (mirrors the inline tick block in
<code class="language-plaintext highlighter-rouge">adsPlay</code>’s main loop). Called once per frame from the fgpilot main
loop. With <code class="language-plaintext highlighter-rouge">delay=2</code>, <code class="language-plaintext highlighter-rouge">islandAnimate</code> advances one wave position every 2
frames and <code class="language-plaintext highlighter-rouge">islandRedrawWave</code> carries the last-drawn wave across the
<code class="language-plaintext highlighter-rouge">grRestoreBgFromRects</code>-wiped frames in between.</p>

<p>Also: seed the clean baseline by calling <code class="language-plaintext highlighter-rouge">islandAnimate</code> 4× inside
<code class="language-plaintext highlighter-rouge">adsPilotEnableWaveBackdrop</code> before the clean backup — same trick that
<code class="language-plaintext highlighter-rouge">islandInit</code> uses in the normal path — so the restored baseline already
has all three wave positions and they look parallel from the first
frame.</p>

<h3 id="3-rect-based-clean-backup-option-b">3. Rect-based clean backup (option B)</h3>

<p>Added <code class="language-plaintext highlighter-rouge">grSaveCleanBgRects()</code> / <code class="language-plaintext highlighter-rouge">grRestoreBgFromRects()</code> to
<code class="language-plaintext highlighter-rouge">graphics_ps1.c</code>. Replaces the 614 KB of full-tile clean copies with a
single 596×152 rectangle at (12, 204) — the union of the foreground pack
bbox and the wave shore area. <strong>~181 KB instead of 614 KB</strong>. That’s the
headroom that lets the BACKGRND PSB coexist with everything else.</p>

<p>After the fix, at compose time all six <code class="language-plaintext highlighter-rouge">malloc</code> probes (4/32/64/93/150
KB) report green — heap is comfortable.</p>

<h3 id="div-by-zero-guard">Div-by-zero guard</h3>

<p><code class="language-plaintext highlighter-rouge">islandAnimate</code> originally did <code class="language-plaintext highlighter-rouge">waveSpriteNo % ttmSlot-&gt;numSprites[0]</code>
with no guard. If <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> ever failed to load, that’s a MIPS
integer-zero-division CPU exception — and on this build the exception
handler effectively hangs. We saw that exact hang several times before
we figured out it wasn’t an I/O wait. The safety-skip in
<code class="language-plaintext highlighter-rouge">adsPilotEnableWaveBackdrop</code> bypasses the seed loop when
<code class="language-plaintext highlighter-rouge">numSprites[0] == 0</code>, so a future load failure becomes “scene plays
without waves” instead of “boot hang”.</p>

<h2 id="open-follow-ups-for-subsequent-sessions">Open follow-ups for subsequent sessions</h2>

<ol>
  <li><strong>Generalize the rect-based clean backup to other scenes.</strong> Each
scene that uses the fgpilot path needs to declare its dynamic rect(s),
and those rects must be computed relative to <code class="language-plaintext highlighter-rouge">islandState.xPos/yPos</code>
and <code class="language-plaintext highlighter-rouge">LEFT_ISLAND</code>. For fishing1 (ELSE branch, no
<code class="language-plaintext highlighter-rouge">storyPrepareSceneBaseByAds</code> call → islandState at origin) absolute
coords work; for any scene that does shift the island, the rects need
to shift too.</li>
  <li><strong>Remove the probe row and heap-tier diagnostic accessors</strong> once we’ve
verified the fix on multiple scenes. They’re useful scaffolding but
add noise to the binary + bg-tile dirty area.</li>
  <li><strong>Audit other <code class="language-plaintext highlighter-rouge">%</code>-by-<code class="language-plaintext highlighter-rouge">numSprites</code> sites</strong> (replay path,
<code class="language-plaintext highlighter-rouge">grRecordReplaySprite</code> lookups, etc.) — any of them could hang if
their backing slot ever turns out empty.</li>
  <li><strong>Consider a further scale-up</strong>: 1/64 grid of bg tiles so compositing
    <ul>
      <li>clean coverage naturally share the same fine-grain regions. The
rect approach is a pragmatic middle step; the finer grid is the
long-run generalization.</li>
    </ul>
  </li>
</ol>
]]></content>
  </entry><entry>
    <title>PS1 Fishing 1 Water Animation Handoff — April 21, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/water-animation-handoff/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/water-animation-handoff/</id>
    <published>2026-04-21T00:00:00-04:00</published>
    <updated>2026-04-21T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Handoff doc for the fishing1 water-animation work on PS1, scoped so the next session can pick up cleanly.</summary>
    <content type="html"><![CDATA[<p>Date: 2026-04-21
Repo: <code class="language-plaintext highlighter-rouge">repo:/</code>
Branch: <code class="language-plaintext highlighter-rouge">ps1</code></p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
  <li><a href="#known-good-baseline" id="markdown-toc-known-good-baseline">Known Good Baseline</a></li>
  <li><a href="#what-has-been-proven" id="markdown-toc-what-has-been-proven">What Has Been Proven</a>    <ul>
      <li><a href="#1-the-wave-primitive-itself-works-on-ps1" id="markdown-toc-1-the-wave-primitive-itself-works-on-ps1">1. The wave primitive itself works on PS1</a></li>
      <li><a href="#2-the-fishing-1-integrated-failure-is-not-a-host-data-absence-issue" id="markdown-toc-2-the-fishing-1-integrated-failure-is-not-a-host-data-absence-issue">2. The Fishing 1 integrated failure is not a host-data absence issue</a></li>
      <li><a href="#3-water-failure-is-integrationruntime-side" id="markdown-toc-3-water-failure-is-integrationruntime-side">3. Water failure is integration/runtime-side</a></li>
    </ul>
  </li>
  <li><a href="#current-technical-findings" id="markdown-toc-current-technical-findings">Current Technical Findings</a>    <ul>
      <li><a href="#grloadbmp-is-ram-path-only-on-ps1" id="markdown-toc-grloadbmp-is-ram-path-only-on-ps1"><code class="language-plaintext highlighter-rouge">grLoadBmp()</code> is RAM-path only on PS1</a></li>
      <li><a href="#integrated-main-loop-shape" id="markdown-toc-integrated-main-loop-shape">Integrated main loop shape</a></li>
      <li><a href="#foreground-pack-path-uses-direct-compositing" id="markdown-toc-foreground-pack-path-uses-direct-compositing">Foreground pack path uses direct compositing</a></li>
      <li><a href="#some-direct-helper-paths-clear-currdirty" id="markdown-toc-some-direct-helper-paths-clear-currdirty">Some direct helper paths clear <code class="language-plaintext highlighter-rouge">currDirty*</code></a></li>
    </ul>
  </li>
  <li><a href="#what-has-been-tried-and-failed" id="markdown-toc-what-has-been-tried-and-failed">What Has Been Tried And Failed</a>    <ul>
      <li><a href="#high-memory-backdrop-frame-streams" id="markdown-toc-high-memory-backdrop-frame-streams">High-memory backdrop frame streams</a></li>
      <li><a href="#draw-pack-replay-variants" id="markdown-toc-draw-pack-replay-variants">Draw-pack replay variants</a></li>
      <li><a href="#native-backdrop-thread-reuse" id="markdown-toc-native-backdrop-thread-reuse">Native backdrop thread reuse</a></li>
      <li><a href="#wrong-path-proof-overlays" id="markdown-toc-wrong-path-proof-overlays">Wrong-path proof overlays</a></li>
    </ul>
  </li>
  <li><a href="#most-important-validated-sandbox-mechanism" id="markdown-toc-most-important-validated-sandbox-mechanism">Most Important Validated Sandbox Mechanism</a></li>
  <li><a href="#current-dirty-worktree-state" id="markdown-toc-current-dirty-worktree-state">Current Dirty Worktree State</a></li>
  <li><a href="#best-current-hypothesis" id="markdown-toc-best-current-hypothesis">Best Current Hypothesis</a></li>
  <li><a href="#recommended-next-debugging-boundary" id="markdown-toc-recommended-next-debugging-boundary">Recommended Next Debugging Boundary</a>    <ul>
      <li><a href="#sandbox-working-path" id="markdown-toc-sandbox-working-path">Sandbox working path</a></li>
      <li><a href="#integrated-failing-path" id="markdown-toc-integrated-failing-path">Integrated failing path</a></li>
    </ul>
  </li>
  <li><a href="#practical-constraint" id="markdown-toc-practical-constraint">Practical Constraint</a></li>
  <li><a href="#short-version" id="markdown-toc-short-version">Short Version</a></li>
</ul>

</details>

<h2 id="goal">Goal</h2>

<p>Get <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> to:</p>

<ul>
  <li>keep the current correct scene-relative foreground/tree occlusion behavior</li>
  <li>keep the current title -&gt; scene handoff</li>
  <li>animate the island water/wave sprites correctly</li>
  <li>do it in a low-memory way that can scale to other scenes</li>
</ul>

<h2 id="known-good-baseline">Known Good Baseline</h2>

<p>The last strong water-free foreground milestone is:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">5ea67b35</code> <code class="language-plaintext highlighter-rouge">Make Fishing 1 foreground packs scene-relative</code></li>
</ul>

<p>This fixed the tree/Johnny mismatch by making the foreground pack scene-relative.</p>

<p>Other later committed milestones that are relevant to the current path:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">44cec0a6</code> <code class="language-plaintext highlighter-rouge">Restore ocean base under foreground runtime</code></li>
  <li><code class="language-plaintext highlighter-rouge">27ac05c9</code> <code class="language-plaintext highlighter-rouge">Keep title visible during staged island base loads</code></li>
  <li><code class="language-plaintext highlighter-rouge">373c38ad</code> <code class="language-plaintext highlighter-rouge">Remove title-screen CD settle delay</code></li>
</ul>

<p>User-validated integrated scene state before wave work:</p>

<ul>
  <li>title screen -&gt; scene handoff works</li>
  <li>ocean + static island base appear</li>
  <li>Johnny foreground pack works</li>
  <li>scene-relative tree occlusion works</li>
  <li>waves do <strong>not</strong> animate</li>
</ul>

<h2 id="what-has-been-proven">What Has Been Proven</h2>

<h3 id="1-the-wave-primitive-itself-works-on-ps1">1. The wave primitive itself works on PS1</h3>

<p>A sandbox worktree was created:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">workspace:/jc_reborn_wave_sandbox</code></li>
  <li>branch <code class="language-plaintext highlighter-rouge">ps1-wave-sandbox</code></li>
</ul>

<p>A minimal <code class="language-plaintext highlighter-rouge">fgpilot wavetest</code> was added there:</p>

<ul>
  <li>black background only</li>
  <li>no island</li>
  <li>no Johnny</li>
  <li>no FG pack</li>
  <li>just 4 <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> wave sprites drawn each frame</li>
</ul>

<p>User validation:</p>

<ul>
  <li>black-screen <code class="language-plaintext highlighter-rouge">wavetest</code> animated correctly</li>
  <li>after one bad island-pop regression, the corrected version was “super legit working”</li>
</ul>

<p>So:</p>

<ul>
  <li>PS1 can animate the wave sprites</li>
  <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> wave frames are valid</li>
  <li>the basic draw path works in isolation</li>
</ul>

<h3 id="2-the-fishing-1-integrated-failure-is-not-a-host-data-absence-issue">2. The Fishing 1 integrated failure is not a host-data absence issue</h3>

<p>Earlier investigations established:</p>

<ul>
  <li>host source contains moving water pixels / wave draw state</li>
  <li>earlier <code class="language-plaintext highlighter-rouge">BG1</code> / backdrop pack experiments did generate changing data</li>
  <li>wave data was not simply “missing”</li>
</ul>

<h3 id="3-water-failure-is-integrationruntime-side">3. Water failure is integration/runtime-side</h3>

<p>Because black-screen <code class="language-plaintext highlighter-rouge">wavetest</code> works but integrated <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> does not, the bug is in the integrated scene path:</p>

<ul>
  <li>clean base construction / restore semantics</li>
  <li>when dynamic water is drawn relative to that base</li>
  <li>or how integrated runtime composition/upload suppresses or overwrites those changes</li>
</ul>

<h2 id="current-technical-findings">Current Technical Findings</h2>

<h3 id="grloadbmp-is-ram-path-only-on-ps1"><code class="language-plaintext highlighter-rouge">grLoadBmp()</code> is RAM-path only on PS1</h3>

<p>In <code class="language-plaintext highlighter-rouge">graphics_ps1.c</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grLoadBmp()</code> immediately calls <code class="language-plaintext highlighter-rouge">grLoadBmpRAM()</code></li>
  <li>the old VRAM/OT path is disabled</li>
</ul>

<p>That means <code class="language-plaintext highlighter-rouge">grDrawSprite()</code> for <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> in PS1 normally goes through:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grCompositeToBackground()</code></li>
  <li>dirty-rect marking</li>
  <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload</li>
</ul>

<p>So this is <strong>not</strong> an OT-vs-RAM issue anymore.</p>

<h3 id="integrated-main-loop-shape">Integrated main loop shape</h3>

<p><code class="language-plaintext highlighter-rouge">fgPlayOceanRuntimeScene()</code> currently does:</p>

<ol>
  <li>build static scene base</li>
  <li><code class="language-plaintext highlighter-rouge">grSaveCleanBgTiles()</code></li>
  <li>start foreground runtime</li>
  <li>frame loop:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">grBeginFrame()</code></li>
      <li><code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code></li>
      <li><code class="language-plaintext highlighter-rouge">grUpdateDisplay(NULL, NULL, NULL)</code></li>
      <li><code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeAdvance()</code></li>
    </ul>
  </li>
</ol>

<p>Inside <code class="language-plaintext highlighter-rouge">grUpdateDisplay()</code>:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">VSync(0)</code></li>
  <li>if <code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeActive()</code> -&gt; <code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeCompose()</code></li>
  <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
  <li><code class="language-plaintext highlighter-rouge">eventsWaitTick(grUpdateDelay)</code></li>
</ol>

<p>So integrated dynamic wave drawing must survive:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code></li>
  <li>any draws inside <code class="language-plaintext highlighter-rouge">foregroundPilotRuntimeCompose()</code></li>
  <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload</li>
</ul>

<h3 id="foreground-pack-path-uses-direct-compositing">Foreground pack path uses direct compositing</h3>

<p>Foreground pack frames go through:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fgBlit16ToBackgroundRect()</code></li>
  <li><code class="language-plaintext highlighter-rouge">grCompositeDirect16ToBackground()</code></li>
</ul>

<p>That path marks dirty rows directly and does <strong>not</strong> use sprite OT primitives.</p>

<h3 id="some-direct-helper-paths-clear-currdirty">Some direct helper paths clear <code class="language-plaintext highlighter-rouge">currDirty*</code></h3>

<p>In <code class="language-plaintext highlighter-rouge">graphics_ps1.c</code>, these functions explicitly zero <code class="language-plaintext highlighter-rouge">currDirtyMinY/MaxY</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grRestoreBackgroundRectForFrame()</code></li>
  <li><code class="language-plaintext highlighter-rouge">grRestoreAndCompositeDirect16BackgroundRectForFrame()</code></li>
</ul>

<p>Those are dangerous in mixed-draw scenarios because they can wipe previously dirtied rows in the same frame.</p>

<p>The main integrated scene-pack path is currently using <code class="language-plaintext highlighter-rouge">grCompositeDirect16ToBackground()</code>, which does not clear <code class="language-plaintext highlighter-rouge">currDirty*</code>, but this area remains suspect and should be watched carefully.</p>

<h2 id="what-has-been-tried-and-failed">What Has Been Tried And Failed</h2>

<h3 id="high-memory-backdrop-frame-streams">High-memory backdrop frame streams</h3>

<p>Tried:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BG1</code> 16-bit backdrop frame packs</li>
  <li>dense backdrop frame playback</li>
  <li>catch-up logic on backdrop frame playback</li>
</ul>

<p>Outcome:</p>

<ul>
  <li>waves still did not visibly animate</li>
  <li>memory usage ballooned</li>
  <li>one prelude-baseline attempt blew <code class="language-plaintext highlighter-rouge">FISHING1.BG1</code> up from about <code class="language-plaintext highlighter-rouge">6.6 MB</code> to about <code class="language-plaintext highlighter-rouge">75 MB</code></li>
  <li>this path also increased crash / blank-screen risk</li>
</ul>

<p>Conclusion:</p>

<ul>
  <li>full-frame backdrop streams are the wrong memory model for PS1 here</li>
</ul>

<h3 id="draw-pack-replay-variants">Draw-pack replay variants</h3>

<p>Tried:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FOC</code>/draw-pack backdrop replay</li>
  <li>host visible-draw replay</li>
  <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> draw-list replay</li>
  <li>sparse and dense timing variations</li>
</ul>

<p>Outcome:</p>

<ul>
  <li>user repeatedly observed absolutely no water motion</li>
</ul>

<p>Conclusion:</p>

<ul>
  <li>asset-side variations were not fixing the actual runtime integration bug</li>
</ul>

<h3 id="native-backdrop-thread-reuse">Native backdrop thread reuse</h3>

<p>Tried:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">adsPilotComposeIslandBackdrop()</code></li>
  <li><code class="language-plaintext highlighter-rouge">adsPilotAdvanceIslandBackdrop()</code></li>
  <li><code class="language-plaintext highlighter-rouge">islandComposePilotWaves()</code></li>
  <li>related backdrop-thread timing/catch-up variants</li>
</ul>

<p>Outcome:</p>

<ul>
  <li>user still saw no water animation in <code class="language-plaintext highlighter-rouge">fishing1</code></li>
</ul>

<h3 id="wrong-path-proof-overlays">Wrong-path proof overlays</h3>

<p>Several “proof” attempts were invalid or misleading:</p>

<ol>
  <li>Fresh per-runtime <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> proof slot
    <ul>
      <li>bad because <code class="language-plaintext highlighter-rouge">grLoadBmpRAM()</code> can silently short-load or fail under memory pressure</li>
      <li>also made the build extremely slow</li>
    </ul>
  </li>
  <li>Scene-state / tide / island-position guesses
    <ul>
      <li>user correctly pushed back on these as not explaining total absence of motion</li>
    </ul>
  </li>
  <li>Repainting waves over sprites with a new slot
    <ul>
      <li>also invalid under memory pressure if the slot never loaded</li>
    </ul>
  </li>
</ol>

<h2 id="most-important-validated-sandbox-mechanism">Most Important Validated Sandbox Mechanism</h2>

<p>The black-screen sandbox that worked used a very simple loop:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">fgInitVisiblePipeline()</code></li>
  <li><code class="language-plaintext highlighter-rouge">fgInitBlackBackground()</code></li>
  <li>preload <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code> once</li>
  <li>each frame:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">grBeginFrame()</code></li>
      <li><code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code></li>
      <li><code class="language-plaintext highlighter-rouge">grDrawSprite(grBackgroundSfc, &amp;slot, x, y, spriteNo, 0)</code> for 4 waves</li>
      <li><code class="language-plaintext highlighter-rouge">grUpdateDisplay(NULL, NULL, NULL)</code></li>
    </ul>
  </li>
</ol>

<p>That worked.</p>

<p>So the exact delta between:</p>

<ul>
  <li>that sandbox loop</li>
  <li>and integrated <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code></li>
</ul>

<p>is where the bug lives.</p>

<h2 id="current-dirty-worktree-state">Current Dirty Worktree State</h2>

<p>At the time of writing, the worktree is dirty with ongoing wave-debugging changes, including:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ads.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">ads.h</code></li>
  <li><code class="language-plaintext highlighter-rouge">foreground_pilot.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">island.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">island.h</code></li>
  <li><code class="language-plaintext highlighter-rouge">config/ps1/cd_layout.xml</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/export-fishing1-foreground-pilot.sh</code></li>
  <li><code class="language-plaintext highlighter-rouge">ttm.c</code></li>
  <li>generated files like <code class="language-plaintext highlighter-rouge">generated/ps1/foreground/FISHING1.FOC</code></li>
</ul>

<p>The latest attempted diagnostic change was:</p>

<ul>
  <li>remove the fresh proof-slot allocation entirely</li>
  <li>reuse the already-loaded island background slot via:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">adsPilotComposeWaveProofOverlay(uint32 vblankCount)</code></li>
      <li><code class="language-plaintext highlighter-rouge">islandComposePilotProofWaves(struct TTtmThread*, uint32)</code></li>
    </ul>
  </li>
  <li>draw those proof waves before <code class="language-plaintext highlighter-rouge">grUpdateDisplay()</code> in <code class="language-plaintext highlighter-rouge">fgPlayOceanRuntimeScene()</code></li>
</ul>

<p>User result on that family of proof builds:</p>

<ul>
  <li>still no visible wave overlay</li>
  <li>speed badly degraded in some builds</li>
</ul>

<p>So the integrated path is still not surfacing even deliberately wrong wave draws.</p>

<h2 id="best-current-hypothesis">Best Current Hypothesis</h2>

<p>There is a runtime integration bug in the visible frame pipeline, likely one of:</p>

<ol>
  <li>dynamic wave draws are being overwritten later in the same frame</li>
  <li>dirty-row bookkeeping for integrated scene frames is wiping or skipping the wave rows</li>
  <li>clean-base restore + foreground compose ordering is neutralizing the dynamic wave changes</li>
  <li>integrated scene base construction leaves the wrong clean baseline, so live water changes never survive to upload</li>
</ol>

<p>What is <strong>not</strong> currently a strong hypothesis:</p>

<ul>
  <li>missing wave assets</li>
  <li>host export not containing wave data</li>
  <li>PS1 not being able to animate the wave sprites</li>
</ul>

<p>Those have effectively been disproved.</p>

<h2 id="recommended-next-debugging-boundary">Recommended Next Debugging Boundary</h2>

<p>Stop trying new export formats until the integrated runtime path is pinned down.</p>

<p>The right next comparison is:</p>

<h3 id="sandbox-working-path">Sandbox working path</h3>

<ul>
  <li>black base</li>
  <li>draw waves</li>
  <li>upload</li>
</ul>

<h3 id="integrated-failing-path">Integrated failing path</h3>

<ul>
  <li>restore clean scene base</li>
  <li>draw dynamic waves</li>
  <li>draw foreground pack</li>
  <li>upload</li>
</ul>

<p>The next agent should prove exactly where the dynamic wave draw disappears:</p>

<ol>
  <li>Does the integrated loop actually call the wave draw every frame?</li>
  <li>After the wave draw, are the expected dirty rows still marked?</li>
  <li>After foreground pack compose, are those dirty rows still marked?</li>
  <li>Does <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload those rows?</li>
  <li>Is the clean base itself already containing a static wave frame that visually masks the motion?</li>
</ol>

<h2 id="practical-constraint">Practical Constraint</h2>

<p>Keep the solution low-memory.</p>

<p>Do <strong>not</strong> go back to full decoded backdrop frame buffers as the primary solution.</p>

<p>The likely scalable solution is still:</p>

<ul>
  <li>static scene base recipe</li>
  <li>tiny dynamic base-layer draw replay</li>
  <li>visible-pixels-only scene-relative foreground pack</li>
</ul>

<p>But the integrated runtime bug must be fixed first.</p>

<h2 id="short-version">Short Version</h2>

<ul>
  <li>Tree/Johnny foreground is in good shape.</li>
  <li>Water animation works in a black-screen sandbox.</li>
  <li>Water animation does <strong>not</strong> appear in integrated <code class="language-plaintext highlighter-rouge">fishing1</code>.</li>
  <li>This is now clearly an integrated runtime composition/upload bug, not an asset-generation bug.</li>
  <li>The next agent should debug the runtime draw/dirty/upload boundary, not invent more export variations.</li>
</ul>
]]></content>
  </entry><entry>
    <title>Fishing 1 Pixel-Perfect Plan — April 21, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/fishing1-pixel-perfect-plan/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/fishing1-pixel-perfect-plan/</id>
    <published>2026-04-21T00:00:00-04:00</published>
    <updated>2026-04-21T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Plan to bring the fishing1 fgpilot scene to pixel-perfect parity with the original PC version across every islandState variation.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
  <li><a href="#working-mode" id="markdown-toc-working-mode">Working mode</a></li>
  <li><a href="#approach-path-a-bespoke-stay-on-else-branch" id="markdown-toc-approach-path-a-bespoke-stay-on-else-branch">Approach: Path A (bespoke, stay on ELSE branch)</a></li>
  <li><a href="#step-list" id="markdown-toc-step-list">Step list</a>    <ul>
      <li><a href="#step-1--boot-override-tokens-for-islandstate" id="markdown-toc-step-1--boot-override-tokens-for-islandstate">Step 1 — Boot-override tokens for <code class="language-plaintext highlighter-rouge">islandState</code></a></li>
      <li><a href="#step-2--boot-override-for-islandstate-via-bootmodetxt" id="markdown-toc-step-2--boot-override-for-islandstate-via-bootmodetxt">Step 2 — Boot-override for <code class="language-plaintext highlighter-rouge">islandState</code> via <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code></a></li>
      <li><a href="#step-3--high-tide-baseline" id="markdown-toc-step-3--high-tide-baseline">Step 3 — High-tide baseline</a></li>
      <li><a href="#step-4--low-tide" id="markdown-toc-step-4--low-tide">Step 4 — Low-tide</a></li>
      <li><a href="#steps-59--raft-stages" id="markdown-toc-steps-59--raft-stages">Steps 5–9 — Raft stages</a></li>
      <li><a href="#steps-1013--holidays" id="markdown-toc-steps-1013--holidays">Steps 10–13 — Holidays</a></li>
      <li><a href="#step-14--night-mode" id="markdown-toc-step-14--night-mode">Step 14 — Night mode</a></li>
      <li><a href="#step-15--island-position-variations" id="markdown-toc-step-15--island-position-variations">Step 15 — Island position variations</a></li>
      <li><a href="#step-16--anything-else-you-spot" id="markdown-toc-step-16--anything-else-you-spot">Step 16 — Anything else you spot</a></li>
      <li><a href="#step-17--cleanup--handoff" id="markdown-toc-step-17--cleanup--handoff">Step 17 — Cleanup / handoff</a></li>
    </ul>
  </li>
  <li><a href="#after-this-document-is-done" id="markdown-toc-after-this-document-is-done">After this document is done</a></li>
</ul>

</details>

<h2 id="goal">Goal</h2>

<p>Get the fishing1 fgpilot scene visually identical to the original PC
version of Johnny Castaway, covering every <code class="language-plaintext highlighter-rouge">islandState</code>-driven variation
the scene supports (tide, raft stage, holiday, night, island position,
etc.). This is Stage 1 of a larger effort to pixel-perfect every scene.</p>

<h2 id="working-mode">Working mode</h2>

<p>One step at a time. For every step:</p>

<ol>
  <li>I build exactly one targeted change.</li>
  <li>The build + DuckStation launch runs.</li>
  <li>You look at the result and say whether it’s right, and if not, what’s
off.</li>
  <li>We iterate on that step, or move to the next one.</li>
</ol>

<p>Verification is a visual check by eye, comparing against your memory of
the PC version — no screenshot diffing, no automated perceptual
compare, no reference image. If your eye says it’s right, it’s right.</p>

<h2 id="approach-path-a-bespoke-stay-on-else-branch">Approach: Path A (bespoke, stay on ELSE branch)</h2>

<p>We tried switching to the IF branch (full <code class="language-plaintext highlighter-rouge">adsInitIsland</code>) first — it
produced multiple regressions (tree missing, Johnny pack mis-aligned,
BACKGRND failing to re-load after <code class="language-plaintext highlighter-rouge">ttmInitSlot</code> zeroed our preload).
Reverted.</p>

<p>Instead we stay on the current <strong>ELSE branch</strong> (known-good working state
from commit <code class="language-plaintext highlighter-rouge">74bb0c24</code>: waves animate, scene plays through, no
ghosting) and <strong>add <code class="language-plaintext highlighter-rouge">islandState</code>-driven variations on top of it
manually</strong>. We keep the memory layout we already tuned (preloaded
BACKGRND in <code class="language-plaintext highlighter-rouge">ttmBackgroundSlot</code>, rect-based clean backup).</p>

<p>Important note: <strong>fgpilot mode does not run the original <code class="language-plaintext highlighter-rouge">FISHING.ADS</code>
script or its TTMs.</strong> Johnny’s actions come from the pre-captured
foreground pack (<code class="language-plaintext highlighter-rouge">FISHING1.FG1</code>) — pixel snapshots from the host build.
Scene base drawing (island, trunk, raft, clouds, waves, holiday
overlay) comes from <code class="language-plaintext highlighter-rouge">island.c</code> functions (<code class="language-plaintext highlighter-rouge">islandInit</code>,
<code class="language-plaintext highlighter-rouge">islandAnimate</code>, <code class="language-plaintext highlighter-rouge">islandInitHoliday</code>). We call these directly with a
controlled <code class="language-plaintext highlighter-rouge">islandState</code> in our ELSE-branch glue. Result is pixel-
identical to calling <code class="language-plaintext highlighter-rouge">adsInitIsland</code> — same drawing code, just
different glue.</p>

<h2 id="step-list">Step list</h2>

<h3 id="step-1--boot-override-tokens-for-islandstate">Step 1 — Boot-override tokens for <code class="language-plaintext highlighter-rouge">islandState</code></h3>

<p>Without this we can’t reliably test each variation. The <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code>
parser already handles <code class="language-plaintext highlighter-rouge">fgpilot &lt;scene&gt;</code> — extend it to accept
key=value tokens after the scene name:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fgpilot fishing1 day=N tide=low|high raft=N holiday=N night=0|1 \
                 islandx=N islandy=N
</code></pre></div></div>

<p>Most of these map to existing forced-variables (<code class="language-plaintext highlighter-rouge">storyForcedCurrentDay</code>,
<code class="language-plaintext highlighter-rouge">hostForcedLowTide</code>, <code class="language-plaintext highlighter-rouge">hostForcedRaftStage</code>, <code class="language-plaintext highlighter-rouge">hostForcedIslandX/Y</code>). Some
need new hooks (<code class="language-plaintext highlighter-rouge">holiday</code>, <code class="language-plaintext highlighter-rouge">night</code>). Parse in <code class="language-plaintext highlighter-rouge">jc_reborn.c</code>’s boot-
override handling and apply to <code class="language-plaintext highlighter-rouge">islandState</code> at the top of
<code class="language-plaintext highlighter-rouge">fgPlayOceanRuntimeScene</code> (after the current preload, before the ELSE
branch draws).</p>

<p><strong>You verify</strong>: setting <code class="language-plaintext highlighter-rouge">fgpilot fishing1 tide=low</code> visibly flips wave
positions to the low-tide set; setting <code class="language-plaintext highlighter-rouge">night=1</code> flips to <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code>;
etc. (If the ELSE branch doesn’t yet respond to a token, we just see no
change — that’s fine, the step is “parser wired, overrides present,
ready to drive subsequent steps”.)</p>

<h3 id="step-2--boot-override-for-islandstate-via-bootmodetxt">Step 2 — Boot-override for <code class="language-plaintext highlighter-rouge">islandState</code> via <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code></h3>

<p>Add parse tokens so we can pin variations for testing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fgpilot fishing1 day=N tide=low|high raft=N holiday=N night=0|1 \
                 islandx=N islandy=N
</code></pre></div></div>

<p>Most of these already correspond to existing <code class="language-plaintext highlighter-rouge">storyForced*</code> /
<code class="language-plaintext highlighter-rouge">hostForced*</code> variables — we just need the <code class="language-plaintext highlighter-rouge">BOOTMODE</code> parser to recognize
the <code class="language-plaintext highlighter-rouge">key=value</code> tokens and feed them in. Without this we can’t reliably
exercise variations one at a time.</p>

<p><strong>You verify</strong>: each token actually changes the rendered scene as
expected.</p>

<h3 id="step-3--high-tide-baseline">Step 3 — High-tide baseline</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fgpilot fishing1 day=1 tide=high raft=1 holiday=0 night=0 islandx=0 islandy=0
</code></pre></div></div>

<p><strong>You verify</strong>: looks like high-tide fishing1 on the PC.</p>

<h3 id="step-4--low-tide">Step 4 — Low-tide</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fgpilot fishing1 day=1 tide=low raft=1 holiday=0 night=0 islandx=0 islandy=0
</code></pre></div></div>

<p><strong>You verify</strong>.</p>

<h3 id="steps-59--raft-stages">Steps 5–9 — Raft stages</h3>

<p><code class="language-plaintext highlighter-rouge">raft=1</code>, <code class="language-plaintext highlighter-rouge">raft=2</code>, <code class="language-plaintext highlighter-rouge">raft=3</code>, <code class="language-plaintext highlighter-rouge">raft=4</code>, <code class="language-plaintext highlighter-rouge">raft=5</code>. One step per stage.
<strong>You verify each</strong>.</p>

<h3 id="steps-1013--holidays">Steps 10–13 — Holidays</h3>

<ul>
  <li>10: <code class="language-plaintext highlighter-rouge">holiday=1</code> — Halloween</li>
  <li>11: <code class="language-plaintext highlighter-rouge">holiday=2</code> — St. Patrick’s</li>
  <li>12: <code class="language-plaintext highlighter-rouge">holiday=3</code> — Christmas</li>
  <li>13: <code class="language-plaintext highlighter-rouge">holiday=4</code> — New Year</li>
</ul>

<p><strong>You verify each</strong>.</p>

<h3 id="step-14--night-mode">Step 14 — Night mode</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fgpilot fishing1 day=1 tide=high raft=1 holiday=0 night=1 islandx=0 islandy=0
</code></pre></div></div>

<p><strong>You verify</strong>.</p>

<h3 id="step-15--island-position-variations">Step 15 — Island position variations</h3>

<p>A couple of sample <code class="language-plaintext highlighter-rouge">(islandx, islandy)</code> pairs from the <code class="language-plaintext highlighter-rouge">VARPOS_OK</code> ranges
the original randomizer uses:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">islandx=-200 islandy=-20</code></li>
  <li><code class="language-plaintext highlighter-rouge">islandx=-60 islandy=-60</code></li>
  <li><code class="language-plaintext highlighter-rouge">islandx=-114 islandy=14</code></li>
</ul>

<p><strong>You verify</strong> each looks sane and matches how the PC places the island.</p>

<h3 id="step-16--anything-else-you-spot">Step 16 — Anything else you spot</h3>

<p>Whatever fishing1 variation or detail you notice is off that isn’t one of
the above — coconuts state, seagulls, boat, banners, rain/weather,
sparkles, special FX, etc. Handled one at a time.</p>

<h3 id="step-17--cleanup--handoff">Step 17 — Cleanup / handoff</h3>

<p>Decide: keep the <code class="language-plaintext highlighter-rouge">BOOTMODE</code> override tokens as a permanent dev
affordance (documented), or gate them behind a debug flag. Either way,
commit a final “pixel-perfect fishing1” marker so we can return to it as
the known-good baseline when we generalize to other scenes.</p>

<h2 id="after-this-document-is-done">After this document is done</h2>

<p>Stage 2 (future): apply the same process to fishing2 and onward; build
the scene-specific dirty-rect table needed for the rect-based clean
backup; generalize island-relative offset math so the rects move with
<code class="language-plaintext highlighter-rouge">islandState.xPos</code> / <code class="language-plaintext highlighter-rouge">LEFT_ISLAND</code>.</p>

<p>Stage 3 (future): Johnny walk transitions between scenes, holiday
toggles at the start-screen level, direct joystick control mode — per
the larger master plan.</p>
]]></content>
  </entry><entry>
    <title>Ocean Restore Plan — April 16, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/ocean-restore-plan/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/ocean-restore-plan/</id>
    <published>2026-04-16T00:00:00-04:00</published>
    <updated>2026-04-16T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Short-term plan for restoring the ocean draw path on the PS1 prerender pilot, with fishing1 as the safe baseline.</summary>
    <content type="html"><![CDATA[<p>Status: active short-term plan
Owner: PS1 prerender pilot</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#safe-baseline" id="markdown-toc-safe-baseline">Safe baseline</a></li>
  <li><a href="#what-just-failed" id="markdown-toc-what-just-failed">What just failed</a></li>
  <li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
  <li><a href="#execution-rules" id="markdown-toc-execution-rules">Execution rules</a></li>
  <li><a href="#step-sequence" id="markdown-toc-step-sequence">Step sequence</a>    <ul>
      <li><a href="#step-1-lock-baseline-again" id="markdown-toc-step-1-lock-baseline-again">Step 1: Lock baseline again</a></li>
      <li><a href="#step-2-prove-ocean-only-initialization-in-isolation" id="markdown-toc-step-2-prove-ocean-only-initialization-in-isolation">Step 2: Prove ocean-only initialization in isolation</a></li>
      <li><a href="#step-3-put-the-existing-standalone-foreground-player-on-top-of-the-proven" id="markdown-toc-step-3-put-the-existing-standalone-foreground-player-on-top-of-the-proven">Step 3: Put the existing standalone foreground player on top of the proven</a></li>
      <li><a href="#ocean-only-base" id="markdown-toc-ocean-only-base">ocean-only base</a></li>
      <li><a href="#step-4-if-step-3-fails-fix-restore-semantics-before-trying-more-init-work" id="markdown-toc-step-4-if-step-3-fails-fix-restore-semantics-before-trying-more-init-work">Step 4: If Step 3 fails, fix restore semantics before trying more init work</a></li>
      <li><a href="#step-5-only-after-ocean-is-stable-restore-island" id="markdown-toc-step-5-only-after-ocean-is-stable-restore-island">Step 5: Only after ocean is stable, restore island</a></li>
      <li><a href="#step-6-only-after-static-island-is-stable-restore-random-island-placement" id="markdown-toc-step-6-only-after-static-island-is-stable-restore-random-island-placement">Step 6: Only after static island is stable, restore random island placement</a></li>
    </ul>
  </li>
  <li><a href="#immediate-next-move" id="markdown-toc-immediate-next-move">Immediate next move</a></li>
</ul>

</details>

<h2 id="safe-baseline">Safe baseline</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">title -&gt; fgpilot fishing1</code> is the current known-good path.</li>
  <li><code class="language-plaintext highlighter-rouge">fishing1</code> prerender playback is visually correct on the black base.</li>
  <li>Timing is close to PC after host-deadline replay and deadline catch-up work.</li>
  <li>The generic foreground runtime path has already been proven on <code class="language-plaintext highlighter-rouge">fishing2</code>.</li>
</ul>

<p>Reference launch:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">./scripts/rebuild-and-let-run.sh fgpilot fishing1</code></li>
</ul>

<h2 id="what-just-failed">What just failed</h2>

<p>These are explicitly not the next step:</p>

<ul>
  <li>swapping ocean into the standalone prerender loop by changing only the base
background load</li>
  <li>routing <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> through full <code class="language-plaintext highlighter-rouge">adsInitIsland() + adsPlay()</code></li>
</ul>

<p>Observed failures:</p>

<ul>
  <li>repeated Johnny overpaint / ghosting when ocean was forced under the
standalone pack player</li>
  <li>partial scene startup and missing Johnny when full ADS scene routing was used</li>
</ul>

<p>Conclusion:</p>

<ul>
  <li>ocean cannot be restored safely by brute-forcing a different background into
the current standalone playback loop</li>
  <li>full ADS scene routing changes too much behavior at once for this milestone</li>
</ul>

<h2 id="goal">Goal</h2>

<p>Restore the real ocean layer under prerender playback while keeping the
foreground pack playback path stable and generic.</p>

<h2 id="execution-rules">Execution rules</h2>

<ol>
  <li>One change at a time.</li>
  <li>Run after every step.</li>
  <li>User validates every step before any commit.</li>
  <li>No Fishing-1-only runtime hacks.</li>
  <li>Do not reintroduce full ADS scene playback as an implicit side effect.</li>
</ol>

<h2 id="step-sequence">Step sequence</h2>

<h3 id="step-1-lock-baseline-again">Step 1: Lock baseline again</h3>

<p>Goal:</p>

<ul>
  <li>keep <code class="language-plaintext highlighter-rouge">title -&gt; fgpilot fishing1</code> on the known-good black-base path</li>
</ul>

<p>Exit criteria:</p>

<ul>
  <li>visuals match the current baseline</li>
  <li>no ocean yet</li>
  <li>no Johnny ghosting</li>
</ul>

<h3 id="step-2-prove-ocean-only-initialization-in-isolation">Step 2: Prove ocean-only initialization in isolation</h3>

<p>Goal:</p>

<ul>
  <li>initialize the ocean/island background machinery without starting scene ADS
logic or foreground playback</li>
</ul>

<p>Requirements:</p>

<ul>
  <li>no walk thread</li>
  <li>no island animation thread unless explicitly needed for the ocean base to
appear</li>
  <li>no foreground prerender overlay yet</li>
</ul>

<p>Exit criteria:</p>

<ul>
  <li>ocean appears correctly</li>
  <li>no corruption or title leftovers</li>
  <li>stable for a simple hold run</li>
</ul>

<h3 id="step-3-put-the-existing-standalone-foreground-player-on-top-of-the-proven">Step 3: Put the existing standalone foreground player on top of the proven</h3>
<h3 id="ocean-only-base">ocean-only base</h3>

<p>Goal:</p>

<ul>
  <li>reuse the exact current standalone <code class="language-plaintext highlighter-rouge">fishing1</code> foreground playback loop</li>
  <li>change only the source of the clean background baseline</li>
</ul>

<p>Exit criteria:</p>

<ul>
  <li>ocean visible</li>
  <li>Johnny and props still render correctly</li>
  <li>no cumulative overpaint / ghosting</li>
</ul>

<h3 id="step-4-if-step-3-fails-fix-restore-semantics-before-trying-more-init-work">Step 4: If Step 3 fails, fix restore semantics before trying more init work</h3>

<p>Primary suspects:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grRestoreBackgroundRectForFrame()</code></li>
  <li><code class="language-plaintext highlighter-rouge">grRestoreAndCompositeDirect16BackgroundRectForFrame()</code></li>
  <li><code class="language-plaintext highlighter-rouge">prevDirty</code> / <code class="language-plaintext highlighter-rouge">currDirty</code> advancement</li>
  <li>assumptions in the standalone loop that only happen to work on black</li>
</ul>

<p>Rule:</p>

<ul>
  <li>do not add more background setup complexity until restore correctness is
proven against a non-black clean baseline</li>
</ul>

<h3 id="step-5-only-after-ocean-is-stable-restore-island">Step 5: Only after ocean is stable, restore island</h3>

<p>Goal:</p>

<ul>
  <li>static island first</li>
  <li>no random placement yet</li>
</ul>

<h3 id="step-6-only-after-static-island-is-stable-restore-random-island-placement">Step 6: Only after static island is stable, restore random island placement</h3>

<p>Goal:</p>

<ul>
  <li>generic island positioning</li>
  <li>foreground playback still composes correctly</li>
</ul>

<h2 id="immediate-next-move">Immediate next move</h2>

<p>Build an <code class="language-plaintext highlighter-rouge">ocean-only</code> proof path that initializes the real ocean background
without entering full ADS scene playback, validate that by itself, then layer
the current standalone <code class="language-plaintext highlighter-rouge">fishing1</code> foreground playback over that same base.</p>
]]></content>
  </entry><entry>
    <title>Current PS1 Prerender Pilot Status — April 16, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/current-status-2026-04-16/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/current-status-2026-04-16/</id>
    <published>2026-04-16T00:00:00-04:00</published>
    <updated>2026-04-16T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Prerender-pilot-era status snapshot from 2026-04-16. Historical context between two validation eras.</summary>
    <content type="html"><![CDATA[<blockquote>
  <p><strong>⚠️ Historical snapshot — not current truth.</strong>
Dated 2026-04-16. Preserved as a prerender-pilot-era status surface
between the restore-pilot era and the fgpilot/full-SFX baseline. For
current status see the <a href="../source/docs/ps1/scene-status/">scene-status shelf page</a>
and <a href="../source/docs/ps1/current-status/">current-status shelf page</a>.</p>
</blockquote>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#validated-now" id="markdown-toc-validated-now">Validated now</a></li>
  <li><a href="#current-blocker" id="markdown-toc-current-blocker">Current blocker</a></li>
  <li><a href="#failed-recent-experiments" id="markdown-toc-failed-recent-experiments">Failed recent experiments</a></li>
  <li><a href="#current-conclusion" id="markdown-toc-current-conclusion">Current conclusion</a></li>
  <li><a href="#working-reference-command" id="markdown-toc-working-reference-command">Working reference command</a></li>
  <li><a href="#guardrails" id="markdown-toc-guardrails">Guardrails</a></li>
</ul>

</details>

<p>Date: 2026-04-16</p>

<h2 id="validated-now">Validated now</h2>

<ul>
  <li>Title screen is restored.</li>
  <li>Default validation flow is <code class="language-plaintext highlighter-rouge">title -&gt; fgpilot fishing1</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">fishing1</code> full-scene foreground playback is visually correct on the black
base.</li>
  <li>Timing is close to PC after host-deadline replay and deadline catch-up work.</li>
  <li>Generic foreground runtime support is proven on <code class="language-plaintext highlighter-rouge">fishing2</code>.</li>
</ul>

<h2 id="current-blocker">Current blocker</h2>

<ul>
  <li>restoring the real ocean layer under prerender playback without regressing the
working foreground path</li>
</ul>

<h2 id="failed-recent-experiments">Failed recent experiments</h2>

<ul>
  <li>
    <p>naive ocean background swaps inside the standalone prerender loop
Result:
repeated Johnny overpaint / ghosting.</p>
  </li>
  <li>
    <p>routing <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> through full <code class="language-plaintext highlighter-rouge">adsInitIsland() + adsPlay()</code>
Result:
startup behavior changed too much, ocean half-loaded, Johnny never appeared.</p>
  </li>
</ul>

<h2 id="current-conclusion">Current conclusion</h2>

<ul>
  <li>the standalone prerender playback path is still the correct foreground pilot
baseline</li>
  <li>ocean must be restored through a narrower background-only path, not by
switching <code class="language-plaintext highlighter-rouge">fgpilot</code> to full ADS scene execution</li>
  <li>the next good milestone is:
<code class="language-plaintext highlighter-rouge">ocean-only base works</code>
then
<code class="language-plaintext highlighter-rouge">ocean + standalone foreground pack works</code></li>
</ul>

<h2 id="working-reference-command">Working reference command</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">./scripts/rebuild-and-let-run.sh fgpilot fishing1</code></li>
</ul>

<h2 id="guardrails">Guardrails</h2>

<ol>
  <li>One change at a time.</li>
  <li>Run after every step.</li>
  <li>Human validation before commit.</li>
  <li>Keep fixes generic.</li>
</ol>
]]></content>
  </entry><entry>
    <title>PS1 Foreground Top-Layer Validation Log — April 13, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/foreground-top-layer-log/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/foreground-top-layer-log/</id>
    <published>2026-04-13T00:00:00-04:00</published>
    <updated>2026-04-13T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Validation log for prerendered foreground-only fgpilot scenes, kept separate from full PS1 scene verification.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#purpose" id="markdown-toc-purpose">Purpose</a></li>
  <li><a href="#current-proven-pilot" id="markdown-toc-current-proven-pilot">Current Proven Pilot</a></li>
  <li><a href="#status-legend" id="markdown-toc-status-legend">Status Legend</a></li>
  <li><a href="#scene-status" id="markdown-toc-scene-status">Scene Status</a>    <ul>
      <li><a href="#activityads" id="markdown-toc-activityads">ACTIVITY.ADS</a></li>
      <li><a href="#buildingads" id="markdown-toc-buildingads">BUILDING.ADS</a></li>
      <li><a href="#fishingads" id="markdown-toc-fishingads">FISHING.ADS</a></li>
      <li><a href="#johnnyads" id="markdown-toc-johnnyads">JOHNNY.ADS</a></li>
      <li><a href="#maryads" id="markdown-toc-maryads">MARY.ADS</a></li>
      <li><a href="#miscgagads" id="markdown-toc-miscgagads">MISCGAG.ADS</a></li>
      <li><a href="#standads" id="markdown-toc-standads">STAND.ADS</a></li>
      <li><a href="#suzyads" id="markdown-toc-suzyads">SUZY.ADS</a></li>
      <li><a href="#visitorads" id="markdown-toc-visitorads">VISITOR.ADS</a></li>
      <li><a href="#walkstufads" id="markdown-toc-walkstufads">WALKSTUF.ADS</a></li>
    </ul>
  </li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

</details>

<h2 id="purpose">Purpose</h2>

<p>Track prerendered foreground-only pilot status separately from the normal PS1
scene verification program.</p>

<p>This log does not mean a scene is fully PS1-verified. A scene should only move
into the normal verified bucket after full-scene behavior, timing, and visual
parity are signed off.</p>

<h2 id="current-proven-pilot">Current Proven Pilot</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FISHING 1</code> / <code class="language-plaintext highlighter-rouge">FISHING.ADS tag 1</code> / scene <code class="language-plaintext highlighter-rouge">17</code>
    <ul>
      <li>status: <code class="language-plaintext highlighter-rouge">top-layer verified</code></li>
      <li>proof: boots through <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> on PS1 ISO in DuckStation and shows
the prerendered Johnny/pole/starfish foreground path correctly</li>
      <li>current asset: full-frame foreground pack (<code class="language-plaintext highlighter-rouge">frame_step = 1</code>, <code class="language-plaintext highlighter-rouge">156</code> source
frames, <code class="language-plaintext highlighter-rouge">156</code> packed frames)</li>
      <li>current limitation: playback is visually correct but runs at about half
speed; background/ocean layer is not yet restored underneath</li>
    </ul>
  </li>
</ul>

<h2 id="status-legend">Status Legend</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">top-layer verified</code> — prerendered foreground path is visibly working on PS1</li>
  <li><code class="language-plaintext highlighter-rouge">export-ready untested</code> — canonical scene exists, but no PS1 foreground pilot
validation has been recorded yet</li>
</ul>

<h2 id="scene-status">Scene Status</h2>

<h3 id="activityads">ACTIVITY.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">0</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">4</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">5</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">6</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 7</code> / tag <code class="language-plaintext highlighter-rouge">7</code> / scene <code class="language-plaintext highlighter-rouge">7</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 8</code> / tag <code class="language-plaintext highlighter-rouge">8</code> / scene <code class="language-plaintext highlighter-rouge">8</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 9</code> / tag <code class="language-plaintext highlighter-rouge">9</code> / scene <code class="language-plaintext highlighter-rouge">9</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 10</code> / tag <code class="language-plaintext highlighter-rouge">10</code> / scene <code class="language-plaintext highlighter-rouge">3</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 11</code> / tag <code class="language-plaintext highlighter-rouge">11</code> / scene <code class="language-plaintext highlighter-rouge">2</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">ACTIVITY 12</code> / tag <code class="language-plaintext highlighter-rouge">12</code> / scene <code class="language-plaintext highlighter-rouge">1</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="buildingads">BUILDING.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">10</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">13</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">12</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">11</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">14</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">16</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 7</code> / tag <code class="language-plaintext highlighter-rouge">7</code> / scene <code class="language-plaintext highlighter-rouge">15</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="fishingads">FISHING.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FISHING 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">17</code> — <code class="language-plaintext highlighter-rouge">top-layer verified</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">18</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">19</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">20</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">21</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">22</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 7</code> / tag <code class="language-plaintext highlighter-rouge">7</code> / scene <code class="language-plaintext highlighter-rouge">23</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">FISHING 8</code> / tag <code class="language-plaintext highlighter-rouge">8</code> / scene <code class="language-plaintext highlighter-rouge">24</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="johnnyads">JOHNNY.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">25</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">26</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">27</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">28</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">29</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">30</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="maryads">MARY.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">MARY 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">31</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">MARY 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">33</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">MARY 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">32</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">MARY 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">34</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">MARY 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">35</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="miscgagads">MISCGAG.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">MISCGAG 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">36</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">MISCGAG 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">37</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="standads">STAND.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">STAND 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">38</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">39</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">40</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">41</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">42</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">43</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 7</code> / tag <code class="language-plaintext highlighter-rouge">7</code> / scene <code class="language-plaintext highlighter-rouge">44</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 8</code> / tag <code class="language-plaintext highlighter-rouge">8</code> / scene <code class="language-plaintext highlighter-rouge">45</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 9</code> / tag <code class="language-plaintext highlighter-rouge">9</code> / scene <code class="language-plaintext highlighter-rouge">46</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 10</code> / tag <code class="language-plaintext highlighter-rouge">10</code> / scene <code class="language-plaintext highlighter-rouge">47</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 11</code> / tag <code class="language-plaintext highlighter-rouge">11</code> / scene <code class="language-plaintext highlighter-rouge">48</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 12</code> / tag <code class="language-plaintext highlighter-rouge">12</code> / scene <code class="language-plaintext highlighter-rouge">49</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 15</code> / tag <code class="language-plaintext highlighter-rouge">15</code> / scene <code class="language-plaintext highlighter-rouge">50</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">STAND 16</code> / tag <code class="language-plaintext highlighter-rouge">16</code> / scene <code class="language-plaintext highlighter-rouge">51</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="suzyads">SUZY.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SUZY 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">52</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">SUZY 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">53</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="visitorads">VISITOR.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">54</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">55</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 4</code> / tag <code class="language-plaintext highlighter-rouge">4</code> / scene <code class="language-plaintext highlighter-rouge">56</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 5</code> / tag <code class="language-plaintext highlighter-rouge">5</code> / scene <code class="language-plaintext highlighter-rouge">59</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 6</code> / tag <code class="language-plaintext highlighter-rouge">6</code> / scene <code class="language-plaintext highlighter-rouge">57</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">VISITOR 7</code> / tag <code class="language-plaintext highlighter-rouge">7</code> / scene <code class="language-plaintext highlighter-rouge">58</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h3 id="walkstufads">WALKSTUF.ADS</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">WALKSTUF 1</code> / tag <code class="language-plaintext highlighter-rouge">1</code> / scene <code class="language-plaintext highlighter-rouge">60</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">WALKSTUF 2</code> / tag <code class="language-plaintext highlighter-rouge">2</code> / scene <code class="language-plaintext highlighter-rouge">61</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
  <li><code class="language-plaintext highlighter-rouge">WALKSTUF 3</code> / tag <code class="language-plaintext highlighter-rouge">3</code> / scene <code class="language-plaintext highlighter-rouge">62</code> — <code class="language-plaintext highlighter-rouge">export-ready untested</code></li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li>scene catalog: <code class="language-plaintext highlighter-rouge">repo:/regtest-references/manifest.csv</code></li>
  <li>pilot rationale: <code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/OFFLINE_SCENE_PLAYBACK_PIVOT_2026-04-12.md</code></li>
</ul>
]]></content>
  </entry><entry>
    <title>Foreground Timing Plan — April 13, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/foreground-timing-plan/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/foreground-timing-plan/</id>
    <published>2026-04-13T00:00:00-04:00</published>
    <updated>2026-04-13T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Plan to bring fgpilot foreground timing back in line with host baseline after the fishing1 visual win.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#what-worked" id="markdown-toc-what-worked">What Worked</a></li>
  <li><a href="#what-failed" id="markdown-toc-what-failed">What Failed</a></li>
  <li><a href="#prioritized-speed-backlog" id="markdown-toc-prioritized-speed-backlog">Prioritized Speed Backlog</a></li>
  <li><a href="#next-target" id="markdown-toc-next-target">Next Target</a></li>
  <li><a href="#repeat-prompt" id="markdown-toc-repeat-prompt">Repeat Prompt</a></li>
</ul>

</details>

<p>Current validated baseline:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> is visually correct.</li>
  <li>Full-scene export is correct.</li>
  <li>Host timing preservation is corrected enough that playback is back near the
earlier fast baseline instead of the over-slow scheduler-sum path.</li>
  <li>Launch path:
<code class="language-plaintext highlighter-rouge">./scripts/rebuild-and-let-run.sh fgpilot fishing1</code></li>
</ul>

<p>Validation rule:</p>

<ol>
  <li>One change at a time.</li>
  <li>Clean rebuild and launch with the repo script.</li>
  <li>Human checks visuals and speed.</li>
  <li>If visuals are good and speed is same or better, commit and push.</li>
</ol>

<h2 id="what-worked">What Worked</h2>

<ul>
  <li>Entry-table preload.</li>
  <li>Bulk-copy compositor fast path.</li>
  <li>Narrow background restore.</li>
  <li>Full-scene <code class="language-plaintext highlighter-rouge">story-single --until-exit</code> export.</li>
  <li>Corrected host-timing preservation in the export pipeline.</li>
  <li>Single-tile upload-path tightening.</li>
  <li>Dedicated one-tile upload branch in <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>.</li>
  <li>Same-bounds fused restore+composite path.</li>
</ul>

<h2 id="what-failed">What Failed</h2>

<ul>
  <li>Direct framebuffer path.</li>
  <li>Diff/direct-delta runtime path.</li>
  <li>Progressive test path.</li>
  <li>Overlap-skip restore as implemented.</li>
  <li>Naive scheduler-delay summing.</li>
  <li>Naive read-ahead / double-buffering.</li>
  <li>Pilot-only black-clear restore replacement.</li>
</ul>

<h2 id="prioritized-speed-backlog">Prioritized Speed Backlog</h2>

<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">grDrawBackground()</code> / upload cost.
Best remaining likely win on the stable path.</p>
  </li>
  <li>
    <p>Safer background restore reduction.
Retry only with stricter invariants than the broken overlap-skip version.</p>
  </li>
  <li>
    <p>Pack-time precompute for runtime blits/restores.
Tile coverage, row offsets, clipped spans.</p>
  </li>
  <li>
    <p>Same-rect / same-shape cached setup.
Reuse setup when consecutive frames share bounds.</p>
  </li>
  <li>
    <p>Held-frame no-work path.
If the frame is held, do the minimum possible.</p>
  </li>
  <li>
    <p>More specialized memcpy-based foreground blit paths.
Keep squeezing the stable compositor path.</p>
  </li>
  <li>
    <p>Better CD read path, but not naive read-ahead.
Any future buffering must preserve exact ownership and frame identity.</p>
  </li>
  <li>
    <p>Pack layout improvements.
Runtime-friendly ordering and alignment.</p>
  </li>
  <li>
    <p>More timing work only after more render throughput wins.
The biggest timing mistake is fixed for now.</p>
  </li>
  <li>
    <p>Display mode experiments last.
Do not revisit direct/progressive until the stable path is exhausted.</p>
  </li>
</ol>

<h2 id="next-target">Next Target</h2>

<p>Stay on the stable compositor path and keep attacking the heavy early-motion
section of the scene, where the tall tree-walk rect still feels slower than the
later action. Favor changes that reduce restore/upload work for larger active
areas before returning to CD buffering experiments.</p>

<h2 id="repeat-prompt">Repeat Prompt</h2>

<blockquote>
  <p>Continue PS1 Fishing 1 foreground speed improvements from the current repo state. Stay on the stable <code class="language-plaintext highlighter-rouge">fgpilot fishing1</code> path only. Make one high-impact safe improvement from <code class="language-plaintext highlighter-rouge">docs/ps1/research/FOREGROUND_TIMING_PLAN_2026-04-13.md</code>, launch it with <code class="language-plaintext highlighter-rouge">./scripts/rebuild-and-let-run.sh fgpilot fishing1</code>, and stop for my validation. Do not use experimental direct framebuffer, diff, or progressive paths unless explicitly asked.</p>
</blockquote>
]]></content>
  </entry><entry>
    <title>Offline Scene Playback Pivot — April 12, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/offline-scene-playback-pivot/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/offline-scene-playback-pivot/</id>
    <published>2026-04-12T00:00:00-04:00</published>
    <updated>2026-04-12T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Proposed pivot to offline scene playback for the PS1 runtime, written at the moment the option was on the table.</summary>
    <content type="html"><![CDATA[<p>Date: 2026-04-12
Status: Proposed pivot
Owner: PS1 runtime / content pipeline</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#why-this-exists" id="markdown-toc-why-this-exists">Why This Exists</a></li>
  <li><a href="#executive-summary" id="markdown-toc-executive-summary">Executive Summary</a></li>
  <li><a href="#hard-storage-reality" id="markdown-toc-hard-storage-reality">Hard Storage Reality</a>    <ul>
      <li><a href="#full-screen-raw-storage-does-not-fit" id="markdown-toc-full-screen-raw-storage-does-not-fit">Full-screen raw storage does not fit</a></li>
      <li><a href="#current-disc-usage-suggests-room-for-a-smarter-offline-format" id="markdown-toc-current-disc-usage-suggests-room-for-a-smarter-offline-format">Current disc usage suggests room for a smarter offline format</a></li>
    </ul>
  </li>
  <li><a href="#recommended-architecture" id="markdown-toc-recommended-architecture">Recommended Architecture</a></li>
  <li><a href="#core-decision" id="markdown-toc-core-decision">Core Decision</a></li>
  <li><a href="#display-model" id="markdown-toc-display-model">Display Model</a></li>
  <li><a href="#pixel-perfect-johnny-requirement" id="markdown-toc-pixel-perfect-johnny-requirement">Pixel-Perfect Johnny Requirement</a></li>
  <li><a href="#proposed-playback-asset-format" id="markdown-toc-proposed-playback-asset-format">Proposed Playback Asset Format</a>    <ul>
      <li><a href="#plate-encoding" id="markdown-toc-plate-encoding">Plate encoding</a></li>
    </ul>
  </li>
  <li><a href="#why-hybrid-plates-are-better-than-full-fmv" id="markdown-toc-why-hybrid-plates-are-better-than-full-fmv">Why Hybrid Plates Are Better Than Full-FMV</a></li>
  <li><a href="#runtime-responsibilities" id="markdown-toc-runtime-responsibilities">Runtime Responsibilities</a></li>
  <li><a href="#variant-handling" id="markdown-toc-variant-handling">Variant Handling</a></li>
  <li><a href="#feasibility-gate" id="markdown-toc-feasibility-gate">Feasibility Gate</a></li>
  <li><a href="#pilot-plan" id="markdown-toc-pilot-plan">Pilot Plan</a>    <ul>
      <li><a href="#pilot-deliverables" id="markdown-toc-pilot-deliverables">Pilot deliverables</a>        <ul>
          <li><a href="#stage-1-offline-analysis" id="markdown-toc-stage-1-offline-analysis">Stage 1: Offline analysis</a></li>
          <li><a href="#stage-2-storage-simulation" id="markdown-toc-stage-2-storage-simulation">Stage 2: Storage simulation</a></li>
          <li><a href="#stage-3-ps1-playback-prototype" id="markdown-toc-stage-3-ps1-playback-prototype">Stage 3: PS1 playback prototype</a></li>
          <li><a href="#stage-4-visual-validation" id="markdown-toc-stage-4-visual-validation">Stage 4: Visual validation</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#costbenefit-compared-to-current-path" id="markdown-toc-costbenefit-compared-to-current-path">Cost/Benefit Compared To Current Path</a></li>
  <li><a href="#benefits" id="markdown-toc-benefits">Benefits</a></li>
  <li><a href="#costs" id="markdown-toc-costs">Costs</a></li>
  <li><a href="#risks" id="markdown-toc-risks">Risks</a>    <ul>
      <li><a href="#risk-1-plate-regions-are-still-too-big" id="markdown-toc-risk-1-plate-regions-are-still-too-big">Risk 1: Plate regions are still too big</a></li>
      <li><a href="#risk-2-compression-artifacts-break-pixel-perfect-johnny" id="markdown-toc-risk-2-compression-artifacts-break-pixel-perfect-johnny">Risk 2: Compression artifacts break “pixel-perfect Johnny”</a></li>
      <li><a href="#risk-3-oceanbackground-interaction-is-more-scene-dependent-than-expected" id="markdown-toc-risk-3-oceanbackground-interaction-is-more-scene-dependent-than-expected">Risk 3: Ocean/background interaction is more scene-dependent than expected</a></li>
      <li><a href="#risk-4-pipeline-takes-too-long-to-build" id="markdown-toc-risk-4-pipeline-takes-too-long-to-build">Risk 4: Pipeline takes too long to build</a></li>
    </ul>
  </li>
  <li><a href="#recommended-product-split" id="markdown-toc-recommended-product-split">Recommended Product Split</a>    <ul>
      <li><a href="#class-a-offline-playback-scenes" id="markdown-toc-class-a-offline-playback-scenes">Class A: Offline playback scenes</a></li>
      <li><a href="#class-b-live-runtime-scenes" id="markdown-toc-class-b-live-runtime-scenes">Class B: Live runtime scenes</a></li>
    </ul>
  </li>
  <li><a href="#recommendation" id="markdown-toc-recommendation">Recommendation</a></li>
  <li><a href="#immediate-next-steps" id="markdown-toc-immediate-next-steps">Immediate Next Steps</a></li>
  <li><a href="#decision-gate" id="markdown-toc-decision-gate">Decision Gate</a></li>
</ul>

</details>

<h2 id="why-this-exists">Why This Exists</h2>

<p>The current PS1 rendering effort has spent too much time on runtime
composition correctness:</p>

<ul>
  <li>sprite lifetime</li>
  <li>replay continuity</li>
  <li>restore/upload correctness</li>
  <li>scene handoff state carry</li>
  <li>route-specific startup behavior</li>
</ul>

<p>That work has produced useful tooling and narrowed several boundaries, but it
has not produced proportional product progress. The project goal is not “prove
that a dynamic PS1 compositor can exactly reproduce the PC scene graph at
runtime.” The real goal is to ship a stable PS1 version that:</p>

<ul>
  <li>displays story scenes correctly</li>
  <li>keeps Johnny pixel-perfect</li>
  <li>supports sound, pause, controller skip, and later gameplay work</li>
</ul>

<p>This document evaluates a strategic pivot:</p>

<ul>
  <li>stop treating story scenes as fully dynamic runtime composition problems</li>
  <li>move them to offline-authored playback content</li>
  <li>keep the PS1 runtime focused on simple display/compositing responsibilities</li>
</ul>

<h2 id="executive-summary">Executive Summary</h2>

<p>The naive version of the idea is not viable:</p>

<ul>
  <li>full-frame <code class="language-plaintext highlighter-rouge">640x480</code> prerendered playback for all <code class="language-plaintext highlighter-rouge">63</code> scenes does not fit in
the available CD budget</li>
</ul>

<p>But the underlying direction is viable if narrowed:</p>

<ul>
  <li>use offline-authored scene playback for story scenes</li>
  <li>do not store full-screen raw frames</li>
  <li>store only the animated foreground/actor plates and keep background/ocean as
runtime layers or low-cardinality state sets</li>
  <li>use indexed palettes, rectangle crops, frame deltas, and scene-local packet
streams</li>
</ul>

<p>Recommended pivot:</p>

<ol>
  <li>Keep island/gameplay/menu/controller as live runtime systems.</li>
  <li>Convert story scenes to offline playback contracts.</li>
  <li>Start with a hybrid format:
    <ul>
      <li>static background base</li>
      <li>optional ocean/island runtime layer</li>
      <li>prerendered actor/foreground plates per frame</li>
      <li>per-frame metadata for input windows, pause, skip, and audio sync</li>
    </ul>
  </li>
  <li>Pilot the system on:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">FISHING 1</code></li>
      <li>one <code class="language-plaintext highlighter-rouge">BUILDING</code> scene</li>
      <li>one simpler validated scene</li>
    </ul>
  </li>
</ol>

<p>This is the most credible path to pixel-perfect Johnny without continuing the
current runtime bug treadmill.</p>

<h2 id="hard-storage-reality">Hard Storage Reality</h2>

<h3 id="full-screen-raw-storage-does-not-fit">Full-screen raw storage does not fit</h3>

<p>At <code class="language-plaintext highlighter-rouge">640x480</code>:</p>

<ul>
  <li>16-bit frame: about <code class="language-plaintext highlighter-rouge">614,400</code> bytes (<code class="language-plaintext highlighter-rouge">600 KB</code>)</li>
  <li>8-bit indexed frame: about <code class="language-plaintext highlighter-rouge">307,200</code> bytes (<code class="language-plaintext highlighter-rouge">300 KB</code>)</li>
</ul>

<p>For <code class="language-plaintext highlighter-rouge">63</code> scenes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">5 fps</code> for <code class="language-plaintext highlighter-rouge">30 sec</code> each:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">9,450</code> frames</li>
      <li>about <code class="language-plaintext highlighter-rouge">5.5 GB</code> at 16-bit</li>
      <li>about <code class="language-plaintext highlighter-rouge">2.8 GB</code> at 8-bit</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">10 fps</code> for <code class="language-plaintext highlighter-rouge">30 sec</code> each:
    <ul>
      <li>about <code class="language-plaintext highlighter-rouge">11.1 GB</code> at 16-bit</li>
      <li>about <code class="language-plaintext highlighter-rouge">5.5 GB</code> at 8-bit</li>
    </ul>
  </li>
</ul>

<p>That is before:</p>

<ul>
  <li>holiday variants</li>
  <li>raft/no-raft variants</li>
  <li>alternate island placement/state variants</li>
  <li>duplicate prerender storage for boot/title lead-ins or timing slack</li>
</ul>

<p>Conclusion:</p>

<ul>
  <li>full-screen prerendered <code class="language-plaintext highlighter-rouge">640x480</code> story playback is not CD-feasible</li>
</ul>

<h3 id="current-disc-usage-suggests-room-for-a-smarter-offline-format">Current disc usage suggests room for a smarter offline format</h3>

<p>Current checked-in disc image:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/jcreborn.bin</code> is about <code class="language-plaintext highlighter-rouge">38 MB</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/jc_resources/extracted</code> is about <code class="language-plaintext highlighter-rouge">5.5 MB</code></li>
</ul>

<p>So there is substantial unused disc budget relative to the current project
state. The pivot is only viable if that budget is spent on compressed,
structured playback data, not naive full-screen frames.</p>

<h2 id="recommended-architecture">Recommended Architecture</h2>

<h2 id="core-decision">Core Decision</h2>

<p>Story scenes become offline-authored playback units.</p>

<p>The PS1 runtime no longer tries to reconstruct those scenes from:</p>

<ul>
  <li>live TTM script execution</li>
  <li>live sprite ordering</li>
  <li>replay/recovery continuity</li>
  <li>dynamic restore heuristics</li>
</ul>

<p>Instead, a story scene becomes:</p>

<ul>
  <li>a deterministic playback stream</li>
  <li>with explicit frame timing</li>
  <li>explicit plate placement</li>
  <li>explicit audio sync</li>
  <li>explicit pause/skip/transition metadata</li>
</ul>

<h2 id="display-model">Display Model</h2>

<p>Use a hybrid compositor:</p>

<ol>
  <li>Background layer
    <ul>
      <li>static or low-cardinality background state</li>
      <li>ocean/island can remain runtime if that is cheaper than storing it</li>
    </ul>
  </li>
  <li>Foreground playback layer
    <ul>
      <li>prerendered actor/foreground plate frames</li>
      <li>cropped to the smallest stable region that contains Johnny and other
transient scene elements</li>
    </ul>
  </li>
  <li>UI/pause overlay layer
    <ul>
      <li>live runtime, unaffected by cutscene rendering internals</li>
    </ul>
  </li>
</ol>

<p>This is the key trade:</p>

<ul>
  <li>spend offline preprocessing effort</li>
  <li>to eliminate runtime correctness ambiguity</li>
</ul>

<h2 id="pixel-perfect-johnny-requirement">Pixel-Perfect Johnny Requirement</h2>

<p>This pivot only makes sense if Johnny remains exact.</p>

<p>That requires:</p>

<ul>
  <li>PC-side prerender generation from the real host renderer</li>
  <li>no hand-rebuilt sprite composition on PS1</li>
  <li>no runtime re-simulation of sprite layering</li>
  <li>scene-local palette/plate generation that preserves the exact Johnny pixels
from the PC source frames</li>
</ul>

<p>Johnny is then “pixel-perfect by construction” because PS1 shows the exact
offline-authored plate, not a reconstructed approximation.</p>

<h2 id="proposed-playback-asset-format">Proposed Playback Asset Format</h2>

<p>Use a new scene playback family, conceptually separate from the current pack
system.</p>

<p>Suggested on-disc structure:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SCN/&lt;scene_id&gt;.SPK</code>
scene playback pack</li>
  <li><code class="language-plaintext highlighter-rouge">SCN/&lt;scene_id&gt;.IDX</code>
frame index / timing / metadata</li>
</ul>

<p>Per scene payload:</p>

<ul>
  <li>one background descriptor
    <ul>
      <li>static base image or background-state table</li>
    </ul>
  </li>
  <li>one optional ocean/island descriptor
    <ul>
      <li>if runtime-generated, store only parameters</li>
      <li>if prerendered, store small state atlas, not every full frame</li>
    </ul>
  </li>
  <li>one foreground plate stream
    <ul>
      <li>rectangle bounds per frame</li>
      <li>indexed pixel payload</li>
      <li>optional delta-from-previous payload</li>
    </ul>
  </li>
  <li>one timing table
    <ul>
      <li>display duration</li>
      <li>frame-to-audio timing</li>
      <li>skip allowed / pause allowed markers</li>
    </ul>
  </li>
  <li>one interaction metadata table
    <ul>
      <li>future controller skip windows</li>
      <li>subtitle/caption sync hooks</li>
    </ul>
  </li>
</ul>

<h3 id="plate-encoding">Plate encoding</h3>

<p>Per frame:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">x, y, width, height</code></li>
  <li>palette id</li>
  <li>payload mode
    <ul>
      <li>full indexed blit</li>
      <li>XOR/delta patch</li>
      <li>RLE patch</li>
      <li>repeat previous</li>
    </ul>
  </li>
</ul>

<p>Strong default:</p>

<ul>
  <li>8-bit indexed plates</li>
  <li>scene-local palette</li>
  <li>row-oriented RLE or LZ-style compression</li>
  <li>delta patching only if it materially wins in the pilot</li>
</ul>

<p>Do not start with a fancy codec. Start with the simplest format that yields a
real disc win.</p>

<h2 id="why-hybrid-plates-are-better-than-full-fmv">Why Hybrid Plates Are Better Than Full-FMV</h2>

<p>A full video path would also solve correctness, but it has worse tradeoffs:</p>

<ul>
  <li>more aggressive downscale/compression pressure</li>
  <li>less reuse of existing background behavior</li>
  <li>less flexibility for mixed runtime/interactive content</li>
  <li>more likely to look soft or artifacted</li>
</ul>

<p>Hybrid plates keep the expensive visual correctness where it matters:</p>

<ul>
  <li>Johnny</li>
  <li>foreground actors</li>
  <li>transient props</li>
</ul>

<p>while leaving the broad background/ocean space cheap.</p>

<h2 id="runtime-responsibilities">Runtime Responsibilities</h2>

<p>The PS1 scene playback runtime should only do:</p>

<ol>
  <li>Load scene playback metadata</li>
  <li>Prepare background/ocean layer</li>
  <li>For each playback tick:
    <ul>
      <li>decode plate payload</li>
      <li>blit plate into framebuffer/working surface</li>
      <li>present</li>
    </ul>
  </li>
  <li>Obey pause / skip / exit / controller actions</li>
  <li>Keep audio in sync</li>
</ol>

<p>It should not:</p>

<ul>
  <li>discover scene resources dynamically</li>
  <li>run TTM/ADS logic for visual correctness</li>
  <li>depend on replay continuity</li>
  <li>depend on restore-pilot heuristics</li>
</ul>

<h2 id="variant-handling">Variant Handling</h2>

<p>The user concern is valid:</p>

<ul>
  <li>holidays</li>
  <li>raft/no-raft</li>
  <li>island state changes</li>
</ul>

<p>These should not explode storage if handled correctly.</p>

<p>Recommended rule:</p>

<ul>
  <li>only offline-author the variants that actually produce different visual
outputs in story playback</li>
</ul>

<p>Most scenes should not multiply across every theoretical world state. Instead:</p>

<ul>
  <li>scene compiler determines which background plate family is actually used for
that route</li>
  <li>per-scene manifest records the exact chosen variant</li>
</ul>

<p>Likely storage structure:</p>

<ul>
  <li>common foreground stream reused across variants when possible</li>
  <li>different background descriptors only when truly needed</li>
</ul>

<h2 id="feasibility-gate">Feasibility Gate</h2>

<p>This pivot should be accepted only if the pilot demonstrates all three:</p>

<ol>
  <li>Johnny is exact</li>
  <li>runtime implementation is much simpler than the current compositor path</li>
  <li>disc usage scales acceptably</li>
</ol>

<h2 id="pilot-plan">Pilot Plan</h2>

<p>Pilot scenes:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">FISHING 1</code></li>
  <li>one <code class="language-plaintext highlighter-rouge">BUILDING</code> scene</li>
  <li>one already-healthy scene for baseline comparison</li>
</ol>

<h3 id="pilot-deliverables">Pilot deliverables</h3>

<h4 id="stage-1-offline-analysis">Stage 1: Offline analysis</h4>

<p>For each pilot scene, export from host:</p>

<ul>
  <li>full frame sequence</li>
  <li>per-frame difference against previous frame</li>
  <li>minimal bounding rectangle of changed actor/foreground region</li>
  <li>palette statistics</li>
  <li>background stability regions</li>
</ul>

<p>Questions to answer:</p>

<ul>
  <li>how large is the useful foreground plate rectangle really</li>
  <li>how often can frames be represented as deltas</li>
  <li>how many distinct palettes are needed</li>
  <li>whether ocean/island should remain runtime or be part of the plate stream</li>
</ul>

<h4 id="stage-2-storage-simulation">Stage 2: Storage simulation</h4>

<p>For each pilot, estimate disc size under:</p>

<ol>
  <li>full-screen 8-bit</li>
  <li>cropped 8-bit full-frame plates</li>
  <li>cropped 8-bit delta plates</li>
  <li>cropped 4-bit where acceptable</li>
</ol>

<p>Acceptance target:</p>

<ul>
  <li>hybrid cropped format must be dramatically smaller than full-screen storage</li>
  <li>projected full <code class="language-plaintext highlighter-rouge">63</code> scene rollout must fit well under the practical disc
budget with headroom for sound and future assets</li>
</ul>

<h4 id="stage-3-ps1-playback-prototype">Stage 3: PS1 playback prototype</h4>

<p>Implement a small runtime path that:</p>

<ul>
  <li>loads one pilot <code class="language-plaintext highlighter-rouge">.SPK</code></li>
  <li>shows background</li>
  <li>plays foreground plates</li>
  <li>supports pause and scene skip</li>
</ul>

<p>No attempt at full generic cutscene engine yet.</p>

<h4 id="stage-4-visual-validation">Stage 4: Visual validation</h4>

<p>Compare PS1 playback against host source frames for the pilot:</p>

<ul>
  <li>Johnny exactness</li>
  <li>timing drift</li>
  <li>visible compression artifacts</li>
  <li>frame-edge artifacts around the plate region</li>
</ul>

<h2 id="costbenefit-compared-to-current-path">Cost/Benefit Compared To Current Path</h2>

<h2 id="benefits">Benefits</h2>

<ul>
  <li>eliminates large classes of sprite correctness bugs</li>
  <li>moves complexity to PC preprocessing, where iteration is cheaper</li>
  <li>makes story playback deterministic</li>
  <li>gives a stable base for:
    <ul>
      <li>sound</li>
      <li>pause</li>
      <li>skip</li>
      <li>controller input</li>
      <li>later gameplay work</li>
    </ul>
  </li>
</ul>

<p>Most importantly:</p>

<ul>
  <li>it changes the problem from “simulate PC layering on PS1” to “display the
already-correct scene on PS1”</li>
</ul>

<h2 id="costs">Costs</h2>

<ul>
  <li>requires a new content build pipeline</li>
  <li>requires scene capture/export tooling on PC</li>
  <li>requires a new playback asset format</li>
  <li>may require per-scene authoring exceptions in early rollout</li>
  <li>does not automatically solve interactive island/gameplay rendering</li>
</ul>

<h2 id="risks">Risks</h2>

<h3 id="risk-1-plate-regions-are-still-too-big">Risk 1: Plate regions are still too big</h3>

<p>Mitigation:</p>

<ul>
  <li>prove size on the pilot before broad rollout</li>
</ul>

<h3 id="risk-2-compression-artifacts-break-pixel-perfect-johnny">Risk 2: Compression artifacts break “pixel-perfect Johnny”</h3>

<p>Mitigation:</p>

<ul>
  <li>use lossless indexed storage for Johnny-containing regions</li>
  <li>compress transport, not image quality</li>
</ul>

<h3 id="risk-3-oceanbackground-interaction-is-more-scene-dependent-than-expected">Risk 3: Ocean/background interaction is more scene-dependent than expected</h3>

<p>Mitigation:</p>

<ul>
  <li>allow per-scene choice:
    <ul>
      <li>runtime background</li>
      <li>prerendered background-state set</li>
      <li>full-frame fallback for outliers</li>
    </ul>
  </li>
</ul>

<h3 id="risk-4-pipeline-takes-too-long-to-build">Risk 4: Pipeline takes too long to build</h3>

<p>Mitigation:</p>

<ul>
  <li>do not build the full general system first</li>
  <li>build the single-scene pilot first</li>
</ul>

<h2 id="recommended-product-split">Recommended Product Split</h2>

<p>If this pivot is adopted, split the game into two rendering classes:</p>

<h3 id="class-a-offline-playback-scenes">Class A: Offline playback scenes</h3>

<ul>
  <li>story scenes</li>
  <li>cutscenes</li>
  <li>anything whose correctness is dominated by actor composition</li>
</ul>

<h3 id="class-b-live-runtime-scenes">Class B: Live runtime scenes</h3>

<ul>
  <li>island free movement</li>
  <li>menus</li>
  <li>pause overlay</li>
  <li>controller navigation</li>
  <li>gameplay interactions</li>
</ul>

<p>This split is strategically healthier than insisting one renderer solve both.</p>

<h2 id="recommendation">Recommendation</h2>

<p>Adopt the pivot, but in the hybrid form only.</p>

<p>Do not pursue:</p>

<ul>
  <li>naive full-screen <code class="language-plaintext highlighter-rouge">640x480</code> prerender storage</li>
</ul>

<p>Do pursue:</p>

<ul>
  <li>offline-authored story scene playback</li>
  <li>hybrid background + cropped foreground plate format</li>
  <li>pilot-first validation on <code class="language-plaintext highlighter-rouge">FISHING 1</code></li>
</ul>

<h2 id="immediate-next-steps">Immediate Next Steps</h2>

<ol>
  <li>Build a PC export tool for one scene:
    <ul>
      <li>host frames</li>
      <li>crop rectangles</li>
      <li>delta candidates</li>
      <li>size report</li>
    </ul>
  </li>
  <li>Produce a real storage estimate for <code class="language-plaintext highlighter-rouge">FISHING 1</code></li>
  <li>Decide whether ocean/island remains runtime or becomes part of the playback
asset for that scene</li>
  <li>Implement a minimal PS1 playback prototype for one pilot scene</li>
  <li>Compare it visually against host and current PS1</li>
  <li>Only then decide whether to scale to the full <code class="language-plaintext highlighter-rouge">63</code></li>
</ol>

<h2 id="decision-gate">Decision Gate</h2>

<p>Approve the pivot only if the pilot proves:</p>

<ul>
  <li>Johnny is exact</li>
  <li>runtime is much simpler</li>
  <li>projected full-scene storage is acceptable</li>
  <li>pause/skip/audio integration becomes easier, not harder</li>
</ul>

<p>If those are true, this is a more viable path than continuing the current
runtime-composition effort indefinitely.</p>
]]></content>
  </entry><entry>
    <title>VLM Classifier Plan — March 29, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/vlm-classifier-plan/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/vlm-classifier-plan/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Plan to replace the heuristic caption layer with a hosted multimodal model, posed against the local-classifier alternative.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
  <li><a href="#current-read" id="markdown-toc-current-read">Current Read</a></li>
  <li><a href="#chosen-runtime-direction" id="markdown-toc-chosen-runtime-direction">Chosen Runtime Direction</a></li>
  <li><a href="#architecture" id="markdown-toc-architecture">Architecture</a>    <ul>
      <li><a href="#1-keep-the-reference-bank" id="markdown-toc-1-keep-the-reference-bank">1. Keep the reference bank</a></li>
      <li><a href="#2-new-vlm-analyzer" id="markdown-toc-2-new-vlm-analyzer">2. New VLM analyzer</a></li>
      <li><a href="#3-output-schema" id="markdown-toc-3-output-schema">3. Output schema</a></li>
    </ul>
  </li>
  <li><a href="#immediate-next-steps" id="markdown-toc-immediate-next-steps">Immediate Next Steps</a></li>
  <li><a href="#success-criteria" id="markdown-toc-success-criteria">Success Criteria</a></li>
</ul>

</details>

<h2 id="goal">Goal</h2>

<p>Replace the current heuristic caption layer with a real multimodal model that can answer questions like:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Johnny is fishing off the right side of the dock.</code></li>
  <li><code class="language-plaintext highlighter-rouge">Mary is visible center-left on the beach.</code></li>
  <li><code class="language-plaintext highlighter-rouge">The frame is still title/ocean only; no character is visible.</code></li>
</ul>

<p>The output should be structured JSON, not free-form prose.</p>

<h2 id="current-read">Current Read</h2>

<p>The existing <code class="language-plaintext highlighter-rouge">scripts/vision_classifier.py</code> pipeline is still useful for:</p>

<ul>
  <li>nearest-reference retrieval</li>
  <li>family/scene confusion reporting</li>
  <li>coarse failure mode detection</li>
</ul>

<p>But it is not a true semantic model. Its summaries are derived from foreground heuristics and reference templates, so it cannot reliably identify actors or actions.</p>

<h2 id="chosen-runtime-direction">Chosen Runtime Direction</h2>

<p>Primary target:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">OpenVINO GenAI</code></li>
  <li><code class="language-plaintext highlighter-rouge">llmware/Qwen2.5-VL-3B-Instruct-ov-int4</code></li>
</ul>

<p>Why:</p>

<ul>
  <li>CPU-only path</li>
  <li>packaged wheel exists for Python 3.12 on this machine</li>
  <li>OpenVINO exposes a direct VLM pipeline API</li>
  <li>the model is already converted to OpenVINO and quantized for low-resource inference</li>
</ul>

<p>Fallbacks if memory/runtime is still too heavy:</p>

<ul>
  <li>a smaller OpenVINO-converted VLM, if available</li>
  <li><code class="language-plaintext highlighter-rouge">llama.cpp</code> GGUF path with a smaller multimodal model</li>
</ul>

<h2 id="architecture">Architecture</h2>

<h3 id="1-keep-the-reference-bank">1. Keep the reference bank</h3>

<p>The existing reference bank is still valuable as retrieval context.</p>

<p>For a query frame:</p>

<ol>
  <li>compute nearest reference matches with the bank</li>
  <li>pass those matches into the VLM prompt</li>
  <li>ask the VLM for strict JSON</li>
</ol>

<p>This constrains the model without forcing it to invent semantics from scratch.</p>

<h3 id="2-new-vlm-analyzer">2. New VLM analyzer</h3>

<p>Implemented in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">scripts/vision_vlm.py</code></li>
</ul>

<p>Responsibilities:</p>

<ul>
  <li>load a real VLM</li>
  <li>load an image</li>
  <li>optionally load nearest reference hints from the bank</li>
  <li>prompt for structured semantics</li>
  <li>write machine-readable JSON</li>
  <li>render a review HTML page for sampled frames</li>
</ul>

<h3 id="3-output-schema">3. Output schema</h3>

<p>Target JSON keys:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">screen_type</code></li>
  <li><code class="language-plaintext highlighter-rouge">summary</code></li>
  <li><code class="language-plaintext highlighter-rouge">characters</code></li>
  <li><code class="language-plaintext highlighter-rouge">objects</code></li>
  <li><code class="language-plaintext highlighter-rouge">actions</code></li>
  <li><code class="language-plaintext highlighter-rouge">confidence</code></li>
  <li><code class="language-plaintext highlighter-rouge">notes</code></li>
</ul>

<p>Each character should include:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code></li>
  <li><code class="language-plaintext highlighter-rouge">confidence</code></li>
  <li><code class="language-plaintext highlighter-rouge">position</code></li>
  <li><code class="language-plaintext highlighter-rouge">action</code></li>
</ul>

<h2 id="immediate-next-steps">Immediate Next Steps</h2>

<ol>
  <li>Install runtime:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">scripts/setup-vision-vlm-openvino.sh</code></li>
    </ul>
  </li>
  <li>Download model:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">llmware/Qwen2.5-VL-3B-Instruct-ov-int4</code></li>
    </ul>
  </li>
  <li>Run image smoke tests on hand-picked reference frames.</li>
  <li>Compare captions across strongly distinct scenes:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">FISHING-1</code></li>
      <li><code class="language-plaintext highlighter-rouge">BUILDING-2</code></li>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY-4</code></li>
      <li><code class="language-plaintext highlighter-rouge">MARY-1</code></li>
    </ul>
  </li>
  <li>If quality is acceptable, add sampled-frame VLM analysis for full reference scenes.</li>
</ol>

<h2 id="success-criteria">Success Criteria</h2>

<p>The VLM path is only acceptable if it can reliably separate at least:</p>

<ul>
  <li>title vs ocean vs live scene</li>
  <li>Johnny vs Mary vs no clear character</li>
  <li>fishing vs bathing vs standing vs walking when the frame is visually clear</li>
  <li>wrong-family failures in PS1 runs</li>
</ul>

<p>If it cannot do that, the runtime/model pair should be replaced rather than tuned around.</p>
]]></content>
  </entry><entry>
    <title>Vision Classifier Worklog — March 29, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/vision-classifier-worklog/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/vision-classifier-worklog/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Worklog from the day the local vision classifier moved from plan to a working captioning pipeline.</summary>
    <content type="html"><![CDATA[<p>Date:</p>
<ul>
  <li>2026-03-29</li>
</ul>

<p>Worktree:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug</code></li>
</ul>

<p>Branch:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">worktree-ps1-debug-20260329</code></li>
</ul>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
  <li><a href="#constraints" id="markdown-toc-constraints">Constraints</a></li>
  <li><a href="#decisions" id="markdown-toc-decisions">Decisions</a></li>
  <li><a href="#implemented" id="markdown-toc-implemented">Implemented</a></li>
  <li><a href="#completed-outputs" id="markdown-toc-completed-outputs">Completed Outputs</a></li>
  <li><a href="#artifact-types-now-available" id="markdown-toc-artifact-types-now-available">Artifact Types Now Available</a></li>
  <li><a href="#scale-reached" id="markdown-toc-scale-reached">Scale Reached</a></li>
  <li><a href="#key-improvements-made" id="markdown-toc-key-improvements-made">Key Improvements Made</a></li>
  <li><a href="#current-limits" id="markdown-toc-current-limits">Current Limits</a></li>
  <li><a href="#next-practical-step" id="markdown-toc-next-practical-step">Next Practical Step</a></li>
</ul>

</details>

<h2 id="goal">Goal</h2>

<p>Build a local CPU-only visual classifier pipeline that:</p>

<ul>
  <li>uses the canonical reference scenes as source of truth</li>
  <li>runs under low-memory constraints</li>
  <li>produces structured semantic artifacts</li>
  <li>generates human-reviewable HTML pages</li>
  <li>scales across all <code class="language-plaintext highlighter-rouge">63</code> reference scenes</li>
</ul>

<h2 id="constraints">Constraints</h2>

<ul>
  <li>effectively no usable GPU</li>
  <li>about <code class="language-plaintext highlighter-rouge">8 GB</code> RAM</li>
  <li>avoid heavyweight runtime dependencies</li>
  <li>acceptable to trade speed for robustness</li>
</ul>

<h2 id="decisions">Decisions</h2>

<p>The pipeline is reference-first, not training-first.</p>

<p>It uses:</p>

<ul>
  <li>deterministic visual features</li>
  <li>lightweight per-frame feature vectors</li>
  <li>nearest-reference retrieval against the canonical bank</li>
  <li>semantic summaries derived from reference-bank matches</li>
</ul>

<p>It deliberately does not depend on:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">torch</code></li>
  <li><code class="language-plaintext highlighter-rouge">transformers</code></li>
  <li><code class="language-plaintext highlighter-rouge">sklearn</code></li>
</ul>

<p>Current runtime dependencies are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Pillow</code></li>
  <li><code class="language-plaintext highlighter-rouge">NumPy</code></li>
</ul>

<h2 id="implemented">Implemented</h2>

<p>Primary code:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/scripts/vision_classifier.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/scripts/run-vision-reference-pipeline.sh</code></li>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/scripts/publish-vision-pipeline.py</code></li>
</ul>

<p>Design doc:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/docs/ps1/research/LOCAL_VISION_CLASSIFIER_PLAN_2026-03-29.md</code></li>
</ul>

<h2 id="completed-outputs">Completed Outputs</h2>

<p>Reference bank:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/artifacts/vision-reference-bank-20260329/</code></li>
</ul>

<p>Reference self-check, latest:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4/</code></li>
</ul>

<p>Published top-level entry:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-pipeline-current/index.html</code></li>
</ul>

<h2 id="artifact-types-now-available">Artifact Types Now Available</h2>

<p>For all <code class="language-plaintext highlighter-rouge">63</code> reference scenes:</p>

<ul>
  <li>per-scene <code class="language-plaintext highlighter-rouge">review.html</code></li>
  <li>per-scene <code class="language-plaintext highlighter-rouge">vision-analysis.json</code></li>
  <li>bank-level <code class="language-plaintext highlighter-rouge">index.html</code></li>
  <li>self-check <code class="language-plaintext highlighter-rouge">index.html</code></li>
  <li>quality report</li>
  <li>confusion report</li>
  <li>family report</li>
  <li>inventory JSON</li>
  <li>inventory HTML</li>
  <li>inventory CSV</li>
  <li>strongest-scenes JSON</li>
  <li>weakest-scenes JSON</li>
  <li>top-level manifest JSON</li>
</ul>

<h2 id="scale-reached">Scale Reached</h2>

<p>Reference scenes processed:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">63</code></li>
</ul>

<p>Reference frames indexed:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">13,128</code></li>
</ul>

<h2 id="key-improvements-made">Key Improvements Made</h2>

<ol>
  <li>Built a full bank over all reference frames.</li>
  <li>Added per-run analysis against that bank.</li>
  <li>Added per-scene HTML review outputs.</li>
  <li>Added full self-check over all <code class="language-plaintext highlighter-rouge">63</code> references.</li>
  <li>Tightened label rules to reduce excessive <code class="language-plaintext highlighter-rouge">ocean</code> classification.</li>
  <li>Added quality, confusion, and family-level reports.</li>
  <li>Added export-friendly inventory artifacts and top-level publishing.</li>
</ol>

<h2 id="current-limits">Current Limits</h2>

<p>The pipeline is operational, but semantic quality is still v1.</p>

<p>Known weak areas:</p>

<ul>
  <li>similar <code class="language-plaintext highlighter-rouge">FISHING</code> scenes cross-match each other</li>
  <li>several <code class="language-plaintext highlighter-rouge">STAND</code> scenes remain hard to separate</li>
  <li>some scene families still need stronger actor/sprite semantics</li>
</ul>

<p>So:</p>

<ul>
  <li>pipeline completeness: good</li>
  <li>artifact completeness: good</li>
  <li>semantic precision: improving, not final</li>
</ul>

<h2 id="next-practical-step">Next Practical Step</h2>

<p>Run this exact pipeline against PS1 result directories and produce:</p>

<ul>
  <li>PS1 <code class="language-plaintext highlighter-rouge">review.html</code></li>
  <li>PS1 <code class="language-plaintext highlighter-rouge">vision-analysis.json</code></li>
  <li>PS1 quality summary against the reference bank</li>
  <li>scene ranking by semantic mismatch mode</li>
</ul>

<p>That is the next step that will directly help fix PS1 scene bugs.</p>
]]></content>
  </entry><entry>
    <title>Vision Classifier Usage — March 29, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/vision-classifier-usage/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/vision-classifier-usage/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Operator reference for running the PS1 vision-classifier captioning pipeline end-to-end.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#one-command-reference-pipeline" id="markdown-toc-one-command-reference-pipeline">One-Command Reference Pipeline</a></li>
  <li><a href="#primary-outputs" id="markdown-toc-primary-outputs">Primary Outputs</a></li>
  <li><a href="#useful-reports" id="markdown-toc-useful-reports">Useful Reports</a></li>
  <li><a href="#core-cli" id="markdown-toc-core-cli">Core CLI</a></li>
  <li><a href="#important-note" id="markdown-toc-important-note">Important Note</a></li>
</ul>

</details>

<p>Worktree:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug</code></li>
</ul>

<h2 id="one-command-reference-pipeline">One-Command Reference Pipeline</h2>

<p>Run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/jc_reborn_ps1_debug/scripts/run-vision-reference-pipeline.sh
</code></pre></div></div>

<p>Optional custom output root:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/tmp/jc_reborn_ps1_debug/scripts/run-vision-reference-pipeline.sh <span class="se">\</span>
  repo:/vision-artifacts/custom-reference-pipeline
</code></pre></div></div>

<h2 id="primary-outputs">Primary Outputs</h2>

<p>Top-level published entry:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-pipeline-current/index.html</code></li>
</ul>

<p>Reference bank:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/artifacts/vision-reference-bank-20260329/index.html</code></li>
</ul>

<p>Latest self-check:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4/index.html</code></li>
</ul>

<h2 id="useful-reports">Useful Reports</h2>

<ul>
  <li>quality: <code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4/quality-report.html</code></li>
  <li>confusion: <code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4/confusion-report.html</code></li>
  <li>family: <code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4/family-report.html</code></li>
  <li>inventory: <code class="language-plaintext highlighter-rouge">repo:/vision-artifacts/vision-reference-pipeline-current/scene-inventory.html</code></li>
</ul>

<h2 id="core-cli">Core CLI</h2>

<p>Build bank:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/jc_reborn_ps1_debug/scripts/vision_classifier.py <span class="se">\</span>
  build-reference-bank <span class="se">\</span>
  <span class="nt">--refdir</span> repo:/regtest-references <span class="se">\</span>
  <span class="nt">--outdir</span> /tmp/jc_reborn_ps1_debug/artifacts/vision-reference-bank-20260329
</code></pre></div></div>

<p>Analyze one run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/jc_reborn_ps1_debug/scripts/vision_classifier.py <span class="se">\</span>
  analyze-run <span class="se">\</span>
  <span class="nt">--scene-dir</span> repo:/regtest-references/ACTIVITY-1 <span class="se">\</span>
  <span class="nt">--bank-dir</span> /tmp/jc_reborn_ps1_debug/artifacts/vision-reference-bank-20260329 <span class="se">\</span>
  <span class="nt">--outdir</span> repo:/vision-artifacts/example-run <span class="se">\</span>
  <span class="nt">--expected-scene</span> ACTIVITY-1
</code></pre></div></div>

<p>Analyze all reference scenes against the bank:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/jc_reborn_ps1_debug/scripts/vision_classifier.py <span class="se">\</span>
  analyze-reference-set <span class="se">\</span>
  <span class="nt">--refdir</span> repo:/regtest-references <span class="se">\</span>
  <span class="nt">--bank-dir</span> /tmp/jc_reborn_ps1_debug/artifacts/vision-reference-bank-20260329 <span class="se">\</span>
  <span class="nt">--outdir</span> repo:/vision-artifacts/vision-reference-selfcheck-20260329-v4
</code></pre></div></div>

<p>Publish top-level entry pages:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/jc_reborn_ps1_debug/scripts/publish-vision-pipeline.py
</code></pre></div></div>

<h2 id="important-note">Important Note</h2>

<p>This pipeline is complete on the reference side.
The next meaningful use is PS1 analysis against the built reference bank.</p>
]]></content>
  </entry><entry>
    <title>PS1 Scene Validation Log — March 29, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/validation-log/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/validation-log/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Validation log of all 63 PS1 scenes against the canonical Linux reference, with per-scene findings and build state.</summary>
    <content type="html"><![CDATA[<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#mission" id="markdown-toc-mission">Mission</a></li>
  <li><a href="#key-findings" id="markdown-toc-key-findings">Key Findings</a>    <ul>
      <li><a href="#build-state" id="markdown-toc-build-state">Build State</a></li>
      <li><a href="#timing" id="markdown-toc-timing">Timing</a></li>
      <li><a href="#sweep-1-all-63-scenes-without-seed-story-single-via-storyplay" id="markdown-toc-sweep-1-all-63-scenes-without-seed-story-single-via-storyplay">Sweep 1: All 63 scenes without seed (story single via storyPlay)</a></li>
      <li><a href="#sweep-2-structural-comparison-against-linux-reference" id="markdown-toc-sweep-2-structural-comparison-against-linux-reference">Sweep 2: Structural comparison against Linux reference</a></li>
      <li><a href="#sweep-3-seeded-deterministic-run-in-progress" id="markdown-toc-sweep-3-seeded-deterministic-run-in-progress">Sweep 3: Seeded deterministic run (in progress)</a></li>
    </ul>
  </li>
  <li><a href="#artifacts" id="markdown-toc-artifacts">Artifacts</a></li>
  <li><a href="#commits-this-session" id="markdown-toc-commits-this-session">Commits This Session</a></li>
  <li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>

</details>

<h2 id="mission">Mission</h2>
<p>Validate all 63 PS1 scenes against the canonical Linux reference set.</p>

<h2 id="key-findings">Key Findings</h2>

<h3 id="build-state">Build State</h3>
<ul>
  <li>HEAD (7a3d4b49) had uncommitted cross-file dependencies → broken build</li>
  <li>Rolled back to f2c33ca3 (clean micro-optimizations state)</li>
  <li>Added <code class="language-plaintext highlighter-rouge">story single</code> boot mode + <code class="language-plaintext highlighter-rouge">seed</code> support + <code class="language-plaintext highlighter-rouge">storyPlayBootSceneDirect()</code></li>
  <li>Current working commits: 5b776e5a → 69432ead → d3b36ccc → 8544b664 → 4054755a</li>
</ul>

<h3 id="timing">Timing</h3>
<p>| Phase | Frame | Wall time |
|——-|——-|———–|
| BIOS animation | 300-600 | 5-10 sec |
| Title screen | 1200-2400 | 20-40 sec |
| Ocean/walk | 2400-3000 | 40-50 sec |
| Scene content | 3000+ | 50+ sec |</p>

<p>REGTEST_FRAMES increased from 1800 to 9000 (150 sec emulated).</p>

<h3 id="sweep-1-all-63-scenes-without-seed-story-single-via-storyplay">Sweep 1: All 63 scenes without seed (story single via storyPlay)</h3>
<ul>
  <li>Result: <strong>63 PASS, 0 FAIL</strong></li>
  <li>Method: island content detection (yellow &gt; 1% in any frame)</li>
  <li>All ADS families produce visible island content</li>
</ul>

<h3 id="sweep-2-structural-comparison-against-linux-reference">Sweep 2: Structural comparison against Linux reference</h3>
<ul>
  <li>Result: <strong>63/63 MATCH</strong></li>
  <li>Both PS1 and reference have island + palm tree in every scene</li>
  <li>compare.html generated with side-by-side thumbnails</li>
</ul>

<h3 id="sweep-3-seeded-deterministic-run-in-progress">Sweep 3: Seeded deterministic run (in progress)</h3>
<ul>
  <li>Using <code class="language-plaintext highlighter-rouge">story single &lt;idx&gt; seed 1</code> matching reference metadata</li>
  <li>Direct scene path via storyPlayBootSceneDirect() for proper island setup</li>
  <li>27/63 completed, all PASS so far</li>
  <li>JOHNNY scenes run slowly (~15-25 min per scene due to complex ADS)</li>
</ul>

<h2 id="artifacts">Artifacts</h2>
<ul>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/sweep-63/</code> — unseeded 63-scene captures</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/seeded-63/</code> — seed-1 deterministic captures (in progress)</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/comparison-63/compare.html</code> — visual comparison report</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/comparison-63/compare.json</code> — structured comparison data</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/validated-63/validation.json</code> — validation summary</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-results/reference-classification/classification.json</code> — reference analysis</li>
</ul>

<h2 id="commits-this-session">Commits This Session</h2>
<p>| Hash | Description |
|——|————-|
| 5b776e5a | Restore clean buildable state from f2c33ca3 |
| 69432ead | Add story single boot mode |
| d3b36ccc | Update timing to 9000 frames |
| 8544b664 | 63/63 scenes render with island content |
| 3be7e960 | 63/63 match reference (JSON artifacts) |
| ff8f0fd1 | Full validation pipeline artifacts |
| 08a878da | Reference classification (63/63 island+Johnny) |
| 4054755a | Add seed support + direct scene path |</p>

<h2 id="next-steps">Next Steps</h2>
<ol>
  <li>Wait for seeded sweep to complete</li>
  <li>Generate pixel-level comparison between seeded PS1 and reference frames</li>
  <li>Identify any per-scene rendering differences</li>
  <li>Fix rendering bugs if found</li>
  <li>Commit final validation artifacts</li>
</ol>
]]></content>
  </entry><entry>
    <title>Local Vision Classifier Plan — March 29, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/local-vision-classifier-plan/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/local-vision-classifier-plan/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Plan to run a local vision classifier for PS1 scene captioning, evaluated against a hosted VLM alternative.</summary>
    <content type="html"><![CDATA[<p>Date: 2026-03-29</p>

<p>Worktree:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug</code></li>
</ul>

<p>Branch:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">worktree-ps1-debug-20260329</code></li>
</ul>

<p>Goal:</p>
<ul>
  <li>build a local visual analysis pipeline for PS1 scene validation and debugging</li>
  <li>run under severe local constraints:
    <ul>
      <li>about <code class="language-plaintext highlighter-rouge">8 GB</code> system RAM</li>
      <li>effectively no useful GPU</li>
      <li>CPU-only inference is acceptable</li>
      <li>slow inference is acceptable</li>
    </ul>
  </li>
  <li>use the canonical Linux/reference scene captures as the semantic source of truth</li>
  <li>pre-run the classifier on those references so PS1 evaluation is reference-driven instead of free-form</li>
</ul>

<p>This is not primarily a training problem yet.
The first target is a practical semantic harness that converts frames into structured, comparable scene descriptions.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#executive-recommendation" id="markdown-toc-executive-recommendation">Executive Recommendation</a></li>
  <li><a href="#why-this-is-the-right-shape" id="markdown-toc-why-this-is-the-right-shape">Why This Is The Right Shape</a></li>
  <li><a href="#source-of-truth" id="markdown-toc-source-of-truth">Source Of Truth</a></li>
  <li><a href="#what-the-harness-must-answer" id="markdown-toc-what-the-harness-must-answer">What The Harness Must Answer</a></li>
  <li><a href="#core-design-reference-first-semantic-retrieval" id="markdown-toc-core-design-reference-first-semantic-retrieval">Core Design: Reference-First Semantic Retrieval</a>    <ul>
      <li><a href="#reference-bank-contents" id="markdown-toc-reference-bank-contents">Reference Bank Contents</a></li>
      <li><a href="#runtime-ps1-evaluation" id="markdown-toc-runtime-ps1-evaluation">Runtime PS1 Evaluation</a></li>
    </ul>
  </li>
  <li><a href="#architecture" id="markdown-toc-architecture">Architecture</a>    <ul>
      <li><a href="#stage-0-build-the-reference-index" id="markdown-toc-stage-0-build-the-reference-index">Stage 0: Build The Reference Index</a></li>
      <li><a href="#stage-1-cheap-deterministic-triage" id="markdown-toc-stage-1-cheap-deterministic-triage">Stage 1: Cheap Deterministic Triage</a></li>
      <li><a href="#stage-2-lightweight-embedding-model" id="markdown-toc-stage-2-lightweight-embedding-model">Stage 2: Lightweight Embedding Model</a></li>
      <li><a href="#stage-3-sampled-semantic-explainer" id="markdown-toc-stage-3-sampled-semantic-explainer">Stage 3: Sampled Semantic Explainer</a></li>
      <li><a href="#stage-4-optional-tiny-supervised-classifier" id="markdown-toc-stage-4-optional-tiny-supervised-classifier">Stage 4: Optional Tiny Supervised Classifier</a></li>
    </ul>
  </li>
  <li><a href="#recommended-models-under-8-gb-ram" id="markdown-toc-recommended-models-under-8-gb-ram">Recommended Models Under 8 GB RAM</a>    <ul>
      <li><a href="#1-clip-vit-b32" id="markdown-toc-1-clip-vit-b32">1. CLIP ViT-B/32</a></li>
      <li><a href="#2-florence-2-base-ft" id="markdown-toc-2-florence-2-base-ft">2. Florence-2-base-ft</a></li>
      <li><a href="#3-smolvlm-500m" id="markdown-toc-3-smolvlm-500m">3. SmolVLM-500M</a></li>
      <li><a href="#4-moondream2" id="markdown-toc-4-moondream2">4. moondream2</a></li>
    </ul>
  </li>
  <li><a href="#reference-grounded-labeling-strategy" id="markdown-toc-reference-grounded-labeling-strategy">Reference-Grounded Labeling Strategy</a>    <ul>
      <li><a href="#pass-a-deterministic-label-seed" id="markdown-toc-pass-a-deterministic-label-seed">Pass A: Deterministic Label Seed</a></li>
      <li><a href="#pass-b-nearest-neighbor-clustering" id="markdown-toc-pass-b-nearest-neighbor-clustering">Pass B: Nearest-Neighbor Clustering</a></li>
      <li><a href="#pass-c-sampled-vlm-annotation" id="markdown-toc-pass-c-sampled-vlm-annotation">Pass C: Sampled VLM Annotation</a></li>
      <li><a href="#pass-d-human-correction-loop" id="markdown-toc-pass-d-human-correction-loop">Pass D: Human Correction Loop</a></li>
    </ul>
  </li>
  <li><a href="#how-ps1-comparison-should-work" id="markdown-toc-how-ps1-comparison-should-work">How PS1 Comparison Should Work</a></li>
  <li><a href="#proposed-output-files" id="markdown-toc-proposed-output-files">Proposed Output Files</a></li>
  <li><a href="#what-counts-as-success" id="markdown-toc-what-counts-as-success">What Counts As Success</a></li>
  <li><a href="#recommended-implementation-order" id="markdown-toc-recommended-implementation-order">Recommended Implementation Order</a>    <ul>
      <li><a href="#phase-1" id="markdown-toc-phase-1">Phase 1</a></li>
      <li><a href="#phase-2" id="markdown-toc-phase-2">Phase 2</a></li>
      <li><a href="#phase-3" id="markdown-toc-phase-3">Phase 3</a></li>
      <li><a href="#phase-4" id="markdown-toc-phase-4">Phase 4</a></li>
    </ul>
  </li>
  <li><a href="#immediate-practical-recommendation" id="markdown-toc-immediate-practical-recommendation">Immediate Practical Recommendation</a></li>
  <li><a href="#research-notes" id="markdown-toc-research-notes">Research Notes</a></li>
  <li><a href="#progress-logging" id="markdown-toc-progress-logging">Progress Logging</a></li>
</ul>

</details>

<h2 id="executive-recommendation">Executive Recommendation</h2>

<p>Do this as a reference-first system, not a generic captioning toy.</p>

<p>The classifier should not try to infer the world from scratch for every PS1 frame.
Instead, it should:</p>

<ol>
  <li>extract cheap visual signatures from every canonical reference frame</li>
  <li>precompute lightweight learned embeddings for those reference frames</li>
  <li>assign semantic labels to reference frames once</li>
  <li>evaluate PS1 frames by comparing them against that prepared reference bank</li>
  <li>only use a small VLM on a sampled subset of frames where we need explanations</li>
</ol>

<p>That gives us three layers:</p>

<ol>
  <li>deterministic triage</li>
  <li>reference-bank retrieval and semantic matching</li>
  <li>sampled-frame textual explanation</li>
</ol>

<h2 id="why-this-is-the-right-shape">Why This Is The Right Shape</h2>

<p>We already know a lot:</p>

<ul>
  <li>the canonical scene list</li>
  <li>the canonical reference images</li>
  <li>which frame ranges matter</li>
  <li>the common PS1 failure modes:
    <ul>
      <li>black screen</li>
      <li>title persistence</li>
      <li>ocean-only background</li>
      <li>wrong family</li>
      <li>sprites missing</li>
      <li>sprites present but wrong timing/content</li>
    </ul>
  </li>
</ul>

<p>That means the cheapest useful classifier is not “train a world model.”
It is:</p>

<ul>
  <li>nearest-neighbor against the known good image bank</li>
  <li>plus a small amount of semantic labeling</li>
</ul>

<p>This also fits the machine constraints much better than training or running a large VLM over every frame.</p>

<h2 id="source-of-truth">Source Of Truth</h2>

<p>Canonical reference runs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/regtest-references</code></li>
</ul>

<p>Scene list:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/config/ps1/regtest-scenes.txt</code></li>
</ul>

<p>Related existing scripts:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/build-reference-index.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/calibrate-visual-detect.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/compare-reference-batch.sh</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/render-compare-timeline.py</code></li>
</ul>

<p>Validation target:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">63</code> scenes</li>
</ul>

<p>The classifier is not the source of truth.
The reference captures are.
The classifier is a semantic layer that makes the truth usable for automated triage and debugging.</p>

<h2 id="what-the-harness-must-answer">What The Harness Must Answer</h2>

<p>Per frame, we need a compact schema like:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"screen_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title|black|ocean|island|scene|transition|unknown"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scene_family_guess"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY|BUILDING|WALKSTUF|FISHING|TITLE|unknown"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sprites_visible"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sprite_density"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none|low|medium|high"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"actors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"johnny"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"present"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="s2">"left|center|right|upper-left|upper-right|lower-left|lower-right"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.83</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"objects"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"raft"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"present"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.21</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"action_summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Johnny is standing near the shoreline."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reference_match"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"scene"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY-1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"frame"</span><span class="p">:</span><span class="w"> </span><span class="s2">"frame_06000.png"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"distance"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.12</span><span class="p">,</span><span class="w">
    </span><span class="nl">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.78</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"background only"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"no active sprite composition visible"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Important:</p>

<ul>
  <li>position buckets are enough initially</li>
  <li>we do not need perfect bounding boxes</li>
  <li>we do not need perfect actor names in phase one</li>
  <li>we do need stable “ocean-only vs actual scene” answers</li>
</ul>

<h2 id="core-design-reference-first-semantic-retrieval">Core Design: Reference-First Semantic Retrieval</h2>

<p>The canonical references should be processed offline into a reusable semantic bank.</p>

<h3 id="reference-bank-contents">Reference Bank Contents</h3>

<p>For each canonical frame:</p>

<ul>
  <li>file path</li>
  <li>scene id</li>
  <li>frame number</li>
  <li>cheap deterministic metrics</li>
  <li>embedding vector</li>
  <li>optional text summary</li>
  <li>optional semantic labels</li>
</ul>

<p>Example:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scene_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY-1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"frame"</span><span class="p">:</span><span class="w"> </span><span class="s2">"frame_06000.png"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"embedding_model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"clip-vit-base-patch32"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"embedding"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"metrics"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"mean_rgb"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">34</span><span class="p">,</span><span class="w"> </span><span class="mi">88</span><span class="p">,</span><span class="w"> </span><span class="mi">120</span><span class="p">],</span><span class="w">
    </span><span class="nl">"water_ratio"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.41</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dark_ratio"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.03</span><span class="p">,</span><span class="w">
    </span><span class="nl">"edge_density"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.19</span><span class="p">,</span><span class="w">
    </span><span class="nl">"telemetry_present"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"labels"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"screen_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"scene"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"scene_family_guess"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sprites_visible"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"johnny_present"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"mary_present"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"raft_present"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"caption"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Johnny stands on the beach near the center of the frame."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="runtime-ps1-evaluation">Runtime PS1 Evaluation</h3>

<p>For each PS1 frame:</p>

<ol>
  <li>compute deterministic metrics</li>
  <li>compute embedding</li>
  <li>find nearest reference frames globally and within target scene</li>
  <li>compare semantic labels against nearest reference cluster</li>
  <li>emit:
    <ul>
      <li>best reference match</li>
      <li>semantic mismatch reasons</li>
      <li>confidence</li>
    </ul>
  </li>
</ol>

<p>This gives us answers like:</p>

<ul>
  <li>“PS1 frame most closely matches ACTIVITY-1 frame_06000 background, but sprites are missing”</li>
  <li>“PS1 frame is closer to FISHING than ACTIVITY”</li>
  <li>“PS1 frame is title-like even though story scene should have launched”</li>
</ul>

<h2 id="architecture">Architecture</h2>

<h3 id="stage-0-build-the-reference-index">Stage 0: Build The Reference Index</h3>

<p>Create a new offline indexer:</p>

<ul>
  <li>input:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">regtest-references/</code></li>
    </ul>
  </li>
  <li>output:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">regtest-references/index.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">regtest-references/embeddings/*.npy</code> or a compact shard format</li>
      <li>optional sampled captions</li>
    </ul>
  </li>
</ul>

<p>This stage should:</p>

<ul>
  <li>enumerate all reference frames</li>
  <li>attach scene/frame metadata</li>
  <li>compute cheap deterministic signatures</li>
  <li>compute embeddings once</li>
  <li>optionally generate labels for a sampled subset</li>
</ul>

<h3 id="stage-1-cheap-deterministic-triage">Stage 1: Cheap Deterministic Triage</h3>

<p>Use classical CV and fixed heuristics first for:</p>

<ul>
  <li>black</li>
  <li>title</li>
  <li>ocean-only</li>
  <li>mostly-water</li>
  <li>telemetry presence</li>
  <li>sprite-like foreground density</li>
  <li>transition frames</li>
</ul>

<p>We already have the beginning of this approach in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/calibrate-visual-detect.py</code></li>
</ul>

<p>This stage should be fast and should handle the most common failure modes without any model call.</p>

<h3 id="stage-2-lightweight-embedding-model">Stage 2: Lightweight Embedding Model</h3>

<p>Use a small image embedding model to compare frames to the reference bank.</p>

<p>Recommended default:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">openai/clip-vit-base-patch32</code></li>
</ul>

<p>Why:</p>

<ul>
  <li>mature</li>
  <li>simple</li>
  <li>good enough for zero-shot and embedding retrieval</li>
  <li>much lighter than a full VLM</li>
  <li>can run on CPU</li>
</ul>

<p>This stage should answer:</p>

<ul>
  <li>which reference frame is this closest to</li>
  <li>which scene family cluster is this closest to</li>
  <li>whether PS1 is semantically near the expected reference state</li>
</ul>

<h3 id="stage-3-sampled-semantic-explainer">Stage 3: Sampled Semantic Explainer</h3>

<p>Use a small VLM only on selected frames:</p>

<ul>
  <li>first visible scene frame</li>
  <li>first mismatch frame</li>
  <li>best-match frame</li>
  <li>final frame</li>
  <li>one or two boundary frames around a divergence</li>
</ul>

<p>This stage produces:</p>

<ul>
  <li>short caption</li>
  <li>visible actors/objects</li>
  <li>rough action summary</li>
  <li>“background only” vs “live scene content”</li>
</ul>

<p>Do not run a VLM across every captured frame at first.</p>

<h3 id="stage-4-optional-tiny-supervised-classifier">Stage 4: Optional Tiny Supervised Classifier</h3>

<p>Once we have enough labeled reference and PS1 frames, train a tiny classifier for:</p>

<ul>
  <li>screen_type</li>
  <li>sprites_visible</li>
  <li>johnny_present</li>
  <li>mary_present</li>
  <li>likely_scene_family</li>
</ul>

<p>This stage is optional and should happen only after the earlier pipeline proves which labels are stable and useful.</p>

<h2 id="recommended-models-under-8-gb-ram">Recommended Models Under 8 GB RAM</h2>

<h3 id="1-clip-vit-b32">1. CLIP ViT-B/32</h3>

<p>Model:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">openai/clip-vit-base-patch32</code></li>
</ul>

<p>Role:</p>

<ul>
  <li>default embedding model</li>
  <li>zero-shot classifier for coarse labels</li>
</ul>

<p>Why:</p>

<ul>
  <li>lowest-risk default for a reference-bank system</li>
  <li>good for nearest-neighbor retrieval</li>
  <li>no need to train to get immediate value</li>
</ul>

<p>Use it for labels like:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">title screen</code></li>
  <li><code class="language-plaintext highlighter-rouge">black screen</code></li>
  <li><code class="language-plaintext highlighter-rouge">ocean background only</code></li>
  <li><code class="language-plaintext highlighter-rouge">beach scene with character</code></li>
  <li><code class="language-plaintext highlighter-rouge">Johnny visible</code></li>
  <li><code class="language-plaintext highlighter-rouge">Mary visible</code></li>
  <li><code class="language-plaintext highlighter-rouge">raft visible</code></li>
</ul>

<h3 id="2-florence-2-base-ft">2. Florence-2-base-ft</h3>

<p>Model:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">microsoft/Florence-2-base-ft</code></li>
</ul>

<p>Role:</p>

<ul>
  <li>sampled-frame semantic explainer</li>
  <li>captioning and object-oriented prompting</li>
</ul>

<p>Why:</p>

<ul>
  <li>small by VLM standards</li>
  <li>multi-task prompting is useful</li>
  <li>better explanatory power than CLIP</li>
</ul>

<p>Use it for:</p>

<ul>
  <li>sampled mismatch frames</li>
  <li>human-review summaries</li>
  <li>first-pass pseudo-labeling on the reference set</li>
</ul>

<h3 id="3-smolvlm-500m">3. SmolVLM-500M</h3>

<p>Model:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">HuggingFaceTB/SmolVLM-500M-Instruct</code></li>
</ul>

<p>Role:</p>

<ul>
  <li>fallback low-memory VLM</li>
</ul>

<p>Why:</p>

<ul>
  <li>designed for constrained devices</li>
  <li>smaller than most VLMs</li>
  <li>useful if Florence-2 is too slow or memory-heavy on CPU</li>
</ul>

<h3 id="4-moondream2">4. moondream2</h3>

<p>Model:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">vikhyatk/moondream2</code></li>
</ul>

<p>Role:</p>

<ul>
  <li>optional CPU captioner if its dedicated runtime proves stable locally</li>
</ul>

<p>Why:</p>

<ul>
  <li>plausible CPU memory footprint</li>
  <li>popular for lightweight image description</li>
</ul>

<p>Risk:</p>

<ul>
  <li>more integration complexity than CLIP</li>
  <li>less attractive as the first default in this repo</li>
</ul>

<h2 id="reference-grounded-labeling-strategy">Reference-Grounded Labeling Strategy</h2>

<p>This is where we use our existing knowledge aggressively.</p>

<h3 id="pass-a-deterministic-label-seed">Pass A: Deterministic Label Seed</h3>

<p>For all reference frames, assign cheap seed labels:</p>

<ul>
  <li>black</li>
  <li>title-like</li>
  <li>ocean-heavy</li>
  <li>telemetry-present</li>
  <li>scene-like</li>
</ul>

<h3 id="pass-b-nearest-neighbor-clustering">Pass B: Nearest-Neighbor Clustering</h3>

<p>Compute embeddings for all reference frames and cluster them by:</p>

<ul>
  <li>scene family</li>
  <li>visual phase</li>
  <li>sprite density</li>
</ul>

<p>This gives us prototype groups like:</p>

<ul>
  <li>ACTIVITY beach with Johnny</li>
  <li>ACTIVITY ocean-only intro</li>
  <li>title/menu</li>
  <li>black/transition</li>
  <li>FISHING island</li>
</ul>

<h3 id="pass-c-sampled-vlm-annotation">Pass C: Sampled VLM Annotation</h3>

<p>Do not caption every frame.</p>

<p>Instead caption:</p>

<ul>
  <li>cluster centers</li>
  <li>cluster outliers</li>
  <li>representative frames per scene</li>
  <li>known important frames:
    <ul>
      <li>first visible scene frame</li>
      <li>middle frame</li>
      <li>final frame</li>
    </ul>
  </li>
</ul>

<p>Then propagate those labels through nearest-neighbor groups.</p>

<p>This keeps VLM cost low while still giving us semantic labels for almost the whole reference bank.</p>

<h3 id="pass-d-human-correction-loop">Pass D: Human Correction Loop</h3>

<p>Build review pages so we can quickly correct:</p>

<ul>
  <li>wrong actor names</li>
  <li>wrong family labels</li>
  <li>wrong action summaries</li>
</ul>

<p>Then persist corrected labels back into the reference bank.</p>

<h2 id="how-ps1-comparison-should-work">How PS1 Comparison Should Work</h2>

<p>For a PS1 run:</p>

<ol>
  <li>identify expected reference scene</li>
  <li>for each sampled PS1 frame:
    <ul>
      <li>deterministic triage</li>
      <li>embedding lookup</li>
      <li>nearest reference match within expected scene</li>
      <li>nearest global reference match</li>
    </ul>
  </li>
  <li>emit both:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">best_expected_match</code></li>
      <li><code class="language-plaintext highlighter-rouge">best_global_match</code></li>
    </ul>
  </li>
</ol>

<p>This distinction matters.</p>

<p>Examples:</p>

<ul>
  <li>expected scene says <code class="language-plaintext highlighter-rouge">ACTIVITY-1</code>, but global match says <code class="language-plaintext highlighter-rouge">FISHING-3</code></li>
  <li>expected scene says <code class="language-plaintext highlighter-rouge">ACTIVITY-1</code>, expected-match frame is ocean-only and global-match frame is also ocean-only</li>
  <li>expected scene says <code class="language-plaintext highlighter-rouge">ACTIVITY-1</code>, best expected match is correct background but labels show <code class="language-plaintext highlighter-rouge">sprites_visible = false</code></li>
</ul>

<p>That gives us much better debugging signal than pixel mismatch alone.</p>

<h2 id="proposed-output-files">Proposed Output Files</h2>

<p>Per reference bank build:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">regtest-references/index.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">regtest-references/embeddings/clip-vit-base-patch32.f16.npy</code></li>
  <li><code class="language-plaintext highlighter-rouge">regtest-references/semantic-labels.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">regtest-references/captions.json</code></li>
</ul>

<p>Per analyzed PS1 run:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">vision-analysis.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">vision-summary.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">vision-review.html</code></li>
</ul>

<p>Example <code class="language-plaintext highlighter-rouge">vision-summary.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scene"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY-1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expected_family"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"frames_analyzed"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w">
  </span><span class="nl">"best_expected_match_score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.81</span><span class="p">,</span><span class="w">
  </span><span class="nl">"best_global_match_scene"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACTIVITY-1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dominant_failure_mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"background_only_missing_sprites"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sprite_visible_ratio"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.08</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title_like_ratio"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ocean_only_ratio"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.67</span><span class="p">,</span><span class="w">
  </span><span class="nl">"notes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"Background matches expected intro ocean frames."</span><span class="p">,</span><span class="w">
    </span><span class="s2">"No later frame matches reference frames with visible Johnny sprites."</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="what-counts-as-success">What Counts As Success</h2>

<p>For scene validation, success is not:</p>

<ul>
  <li>same last-frame hash</li>
  <li>same black frame</li>
  <li>“looks close enough”</li>
</ul>

<p>Success is:</p>

<ul>
  <li>PS1 frames retrieve into the expected reference scene</li>
  <li>the semantic labels broadly agree:
    <ul>
      <li>sprites visible when reference has sprites</li>
      <li>same family</li>
      <li>same coarse phase</li>
      <li>same actor presence on key frames</li>
    </ul>
  </li>
</ul>

<p>For the <code class="language-plaintext highlighter-rouge">63</code>-scene target, the harness should let us say:</p>

<ul>
  <li>fully validated</li>
  <li>visually close but semantically wrong</li>
  <li>background-correct but sprite-missing</li>
  <li>wrong-family reroute</li>
  <li>title/black persistence</li>
</ul>

<h2 id="recommended-implementation-order">Recommended Implementation Order</h2>

<h3 id="phase-1">Phase 1</h3>

<p>Implement:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">build_reference_bank.py</code></li>
  <li>deterministic metric extraction</li>
  <li>CLIP embeddings for references</li>
  <li>nearest-neighbor lookup for PS1 frames</li>
</ul>

<p>No training yet.</p>

<h3 id="phase-2">Phase 2</h3>

<p>Implement:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">analyze_run_vision.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">vision-review.html</code></li>
  <li>per-run semantic summaries</li>
</ul>

<h3 id="phase-3">Phase 3</h3>

<p>Add sampled VLM labeling:</p>

<ul>
  <li>Florence-2 default</li>
  <li>SmolVLM fallback</li>
</ul>

<h3 id="phase-4">Phase 4</h3>

<p>Only after enough labels exist:</p>

<ul>
  <li>tiny supervised classifier for stable labels</li>
</ul>

<h2 id="immediate-practical-recommendation">Immediate Practical Recommendation</h2>

<p>Build the first version around:</p>

<ol>
  <li>deterministic metrics</li>
  <li>CLIP embeddings</li>
  <li>nearest reference retrieval</li>
  <li>sampled Florence-2 captions</li>
</ol>

<p>That is the best tradeoff for this machine and this repo.</p>

<p>It uses what we already know instead of pretending we need a bespoke model first.</p>

<h2 id="research-notes">Research Notes</h2>

<p>Useful model pages / references:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">microsoft/Florence-2-base-ft</code></li>
  <li><code class="language-plaintext highlighter-rouge">openai/clip-vit-base-patch32</code></li>
  <li><code class="language-plaintext highlighter-rouge">HuggingFaceTB/SmolVLM-500M-Instruct</code></li>
  <li><code class="language-plaintext highlighter-rouge">vikhyatk/moondream2</code></li>
</ul>

<p>Key design implication from that research:</p>

<ul>
  <li>CLIP-class retrieval should be the backbone</li>
  <li>tiny VLMs should be used as sparse explainers</li>
  <li>reference-bank preprocessing is where we should spend most of the compute</li>
</ul>

<h2 id="progress-logging">Progress Logging</h2>

<p>All implementation work for this classifier should log:</p>

<ul>
  <li>what model was tested</li>
  <li>RAM usage observed</li>
  <li>per-frame inference time</li>
  <li>whether output was actually useful for PS1 debugging</li>
  <li>where false positives happened</li>
</ul>

<p>That log should live under:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_ps1_debug/docs/ps1/research/</code></li>
</ul>
]]></content>
  </entry><entry>
    <title>PS1 Scene Validation And Debug Prompt</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/agent-prompt/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/agent-prompt/</id>
    <published>2026-03-29T00:00:00-04:00</published>
    <updated>2026-03-29T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Mission brief for the PS1 scene-validation debugging agent: validate 63 PS1 scenes against the canonical Linux reference, with auditable artifacts.</summary>
    <content type="html"><![CDATA[<p>You are working in the <code class="language-plaintext highlighter-rouge">jc_reborn</code> repository on the PS1 runtime and test harness.</p>

<p>Your mission is not to produce plausible-looking runs. Your mission is to get to <code class="language-plaintext highlighter-rouge">63</code> actually validated PS1 story scenes against the canonical Linux/reference output, with trustworthy comparison artifacts a human can review.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#primary-goal" id="markdown-toc-primary-goal">Primary Goal</a></li>
  <li><a href="#source-of-truth" id="markdown-toc-source-of-truth">Source Of Truth</a></li>
  <li><a href="#current-expectations" id="markdown-toc-current-expectations">Current Expectations</a></li>
  <li><a href="#comparison-requirements" id="markdown-toc-comparison-requirements">Comparison Requirements</a></li>
  <li><a href="#how-to-compare-correctly" id="markdown-toc-how-to-compare-correctly">How To Compare Correctly</a>    <ul>
      <li><a href="#single-ps1-run-review" id="markdown-toc-single-ps1-run-review">Single PS1 run review</a></li>
      <li><a href="#ps1-vs-reference-comparison" id="markdown-toc-ps1-vs-reference-comparison">PS1 vs reference comparison</a></li>
      <li><a href="#validation-rule" id="markdown-toc-validation-rule">Validation rule</a></li>
    </ul>
  </li>
  <li><a href="#current-harness-concerns-to-investigate" id="markdown-toc-current-harness-concerns-to-investigate">Current Harness Concerns To Investigate</a></li>
  <li><a href="#current-ps1-runtime-debug-context" id="markdown-toc-current-ps1-runtime-debug-context">Current PS1 Runtime Debug Context</a></li>
  <li><a href="#recommended-work-sequence" id="markdown-toc-recommended-work-sequence">Recommended Work Sequence</a></li>
  <li><a href="#logging-requirements" id="markdown-toc-logging-requirements">Logging Requirements</a></li>
  <li><a href="#standards" id="markdown-toc-standards">Standards</a></li>
</ul>

</details>

<h2 id="primary-goal">Primary Goal</h2>

<p>Reach a state where all <code class="language-plaintext highlighter-rouge">63</code> target PS1 story scenes can be:</p>

<ol>
  <li>booted deterministically</li>
  <li>captured reproducibly</li>
  <li>compared against the canonical reference set</li>
  <li>reviewed visually in generated HTML</li>
  <li>marked validated only when they actually match scene content, not just because they share black frames, title frames, or empty/ocean fallback frames</li>
</ol>

<p>Do not count:</p>
<ul>
  <li>black-screen agreement</li>
  <li>title-screen agreement</li>
  <li>empty-ocean agreement</li>
  <li>“close enough” when no scene content launched</li>
</ul>

<p>The standard is validated scene content.</p>

<h2 id="source-of-truth">Source Of Truth</h2>

<p>The baseline source of truth is already present in this repo:</p>

<ul>
  <li>reference root:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-references</code></li>
    </ul>
  </li>
</ul>

<p>This contains the canonical Linux/reference outputs for the scene set. Treat that as authoritative.</p>

<p>Important related files:</p>

<ul>
  <li>reference scene list:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/config/ps1/regtest-scenes.txt</code></li>
    </ul>
  </li>
  <li>single-scene runner:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/scripts/regtest-scene.sh</code></li>
    </ul>
  </li>
  <li>batch compare runner:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/scripts/compare-reference-batch.sh</code></li>
    </ul>
  </li>
  <li>compare renderer:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/scripts/render-compare-timeline.py</code></li>
    </ul>
  </li>
  <li>single-run review renderer:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/scripts/render-regtest-run.py</code></li>
    </ul>
  </li>
</ul>

<h2 id="current-expectations">Current Expectations</h2>

<p>There is still work to do on both:</p>

<ul>
  <li>the harness</li>
  <li>the actual PS1 scene runs</li>
</ul>

<p>Assume the current harness is useful but not fully solved.</p>

<p>In particular, there is an active suspicion that the harness may be stopping too early relative to the real PS1 boot/title lead-in.</p>

<p>Working hypothesis to test:</p>

<ul>
  <li>scene timing windows may need to be increased significantly</li>
  <li>a good first hypothesis is that effective scene timing may need roughly <code class="language-plaintext highlighter-rouge">+35 seconds</code> of extra boot allowance before valid scene comparison begins</li>
</ul>

<p>Do not assume that hypothesis is true. Test it.</p>

<h2 id="comparison-requirements">Comparison Requirements</h2>

<p>For every serious run, produce artifacts that a human can inspect.</p>

<p>At minimum, each scene run should leave:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">result.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">review.html</code></li>
  <li>raw captured frames</li>
  <li>if reference-compared, <code class="language-plaintext highlighter-rouge">compare.json</code></li>
  <li>if reference-compared, <code class="language-plaintext highlighter-rouge">compare.html</code></li>
</ol>

<p>The human must be able to open the HTML and confirm whether the run contains real scene content.</p>

<h2 id="how-to-compare-correctly">How To Compare Correctly</h2>

<h3 id="single-ps1-run-review">Single PS1 run review</h3>

<p>Use:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/regtest-scene.sh</code></li>
</ul>

<p>This now emits:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">result.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">review.html</code></li>
</ul>

<p>The review page is for raw PS1 inspection only. It is not proof of correctness by itself.</p>

<h3 id="ps1-vs-reference-comparison">PS1 vs reference comparison</h3>

<p>Use:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/compare-reference-batch.sh</code></li>
</ul>

<p>This should be the main path for actual validation sweeps against the canonical reference set.</p>

<p>Expected comparison artifacts per scene:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">compare.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">compare.html</code></li>
</ul>

<p>If compare alignment fails, that is not a pass. It means the harness or the run still needs work.</p>

<h3 id="validation-rule">Validation rule</h3>

<p>A scene is only validated when:</p>

<ul>
  <li>the PS1 run clearly reaches the intended scene</li>
  <li>the scene content aligns against the reference in a defensible way</li>
  <li>the generated HTML artifacts support that claim</li>
</ul>

<h2 id="current-harness-concerns-to-investigate">Current Harness Concerns To Investigate</h2>

<p>You should actively test and improve these:</p>

<ol>
  <li>Boot lead-in may be under-budgeted.
    <ul>
      <li>Current scene windows may begin too early.</li>
      <li>Test longer frame budgets and later alignment windows.</li>
    </ul>
  </li>
  <li>Some current comparisons may overweight title/black/ocean contamination.
    <ul>
      <li>The harness must reject invalid anchors instead of fabricating confidence.</li>
    </ul>
  </li>
  <li>HTML output must always exist for human review.
    <ul>
      <li>Keep that invariant.</li>
    </ul>
  </li>
  <li>The “best frame” or “state hash” is not enough.
    <ul>
      <li>Validation must be scene-content driven.</li>
    </ul>
  </li>
  <li>If the compare path falls back, make that obvious.
    <ul>
      <li>Fallback review pages are useful, but they are not validated compare pages.</li>
    </ul>
  </li>
</ol>

<h2 id="current-ps1-runtime-debug-context">Current PS1 Runtime Debug Context</h2>

<p>The active deep bug work has been concentrated on <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>.</p>

<p>Current strongest runtime read:</p>

<ul>
  <li>the live seam is likely in <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code> in:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>it appears stack-layout-sensitive</li>
  <li>exact local ordering of:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">cdPath</code></li>
      <li><code class="language-plaintext highlighter-rouge">cdfile</code>
matters</li>
    </ul>
  </li>
  <li>tiny caller-frame changes around <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> move the runtime between stable bad branches</li>
</ul>

<p>This means:</p>

<ul>
  <li>do not assume a clean logic bug</li>
  <li>suspect UB, overwrite, or stack-sensitive corruption around the pack-file lookup path</li>
</ul>

<p>That said, do not get trapped in only one scene forever if the better immediate win is harness confidence or a different scene that is closer to validated.</p>

<h2 id="recommended-work-sequence">Recommended Work Sequence</h2>

<ol>
  <li>Verify the harness outputs are always reviewable.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">review.html</code> for single runs</li>
      <li><code class="language-plaintext highlighter-rouge">compare.html</code> for reference comparisons</li>
    </ul>
  </li>
  <li>Audit timing assumptions.
    <ul>
      <li>Test whether boot grace, scene-entry windows, or total run length need to move later</li>
      <li>Specifically test the “add roughly 35 seconds” hypothesis</li>
    </ul>
  </li>
  <li>Run targeted scene comparisons against the reference set.
    <ul>
      <li>Prefer one scene at a time when debugging</li>
      <li>Prefer broader sweeps when ranking “closest to validated”</li>
    </ul>
  </li>
  <li>Identify the easiest real win.
    <ul>
      <li>Choose scenes that already show actual content and are closest to aligned reference output</li>
    </ul>
  </li>
  <li>Only then return to deeper runtime surgery when needed.</li>
</ol>

<h2 id="logging-requirements">Logging Requirements</h2>

<p>Keep a concise progress log in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/HARNESS_WORKLOG_2026-03-28.md</code></li>
</ul>

<p>For each significant step, record:</p>

<ul>
  <li>what was changed</li>
  <li>what run was executed</li>
  <li>where the result artifacts are</li>
  <li>what conclusion is justified</li>
  <li>what the next target is</li>
</ul>

<p>Do not write vague summaries. Record the actual result and the actual conclusion.</p>

<h2 id="standards">Standards</h2>

<p>Be skeptical of false positives.</p>

<p>If a scene:</p>

<ul>
  <li>never launches</li>
  <li>only shows water</li>
  <li>only shows black</li>
  <li>only shows title</li>
  <li>or fails to align cleanly against reference</li>
</ul>

<p>then it is not validated.</p>

<p>The end goal is:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">63</code> scenes</li>
  <li>all human-reviewable</li>
  <li>all compared against canonical reference</li>
  <li>all validated on real scene content</li>
</ul>
]]></content>
  </entry><entry>
    <title>Regtest Harness Worklog — March 28, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/harness-worklog/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/harness-worklog/</id>
    <published>2026-03-28T00:00:00-04:00</published>
    <updated>2026-03-28T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Worklog from the rebuild of the PS1 regtest harness onto a scene-sequence comparator over the canonical host baseline corpus.</summary>
    <content type="html"><![CDATA[<p>Goal</p>
<ul>
  <li>Replace the old final-frame/hash-based PS1-vs-reference harness with a scene-sequence harness that compares PS1 output against the canonical host baseline corpus in <code class="language-plaintext highlighter-rouge">regtest-references/</code>.</li>
</ul>

<p>Current status</p>
<ul>
  <li>Canonical host baseline corpus exists in <code class="language-plaintext highlighter-rouge">regtest-references/</code> for all 63 scenes.</li>
  <li>Core sequence comparator has been upgraded to:
    <ul>
      <li>align on scene entry</li>
      <li>separate PS1 <code class="language-plaintext highlighter-rouge">pre-scene</code>, <code class="language-plaintext highlighter-rouge">in-scene</code>, and <code class="language-plaintext highlighter-rouge">post-scene</code> frames</li>
      <li>report <code class="language-plaintext highlighter-rouge">MATCH</code>, <code class="language-plaintext highlighter-rouge">PIXEL_MISMATCH</code>, <code class="language-plaintext highlighter-rouge">TIMING_MISMATCH</code>, or <code class="language-plaintext highlighter-rouge">ALIGNMENT_FAILED</code></li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">regtest-scene.sh</code> can now request sequence-based baseline analysis against a canonical scene directory.</li>
  <li><code class="language-plaintext highlighter-rouge">compare-reference-batch.sh</code> has been rewired toward sequence comparison and canonical reference defaults.</li>
  <li><code class="language-plaintext highlighter-rouge">build-ps1.sh</code> now recreates <code class="language-plaintext highlighter-rouge">build-ps1/</code> if cleanup removed it.</li>
  <li>Raw PS1 output roots with nested Docker frame directories are now discovered correctly by <code class="language-plaintext highlighter-rouge">compare-sequence-runs.py</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">regtest-scene.sh</code> now has a <code class="language-plaintext highlighter-rouge">--fast-baseline</code> mode for interactive validation runs that skips full telemetry/visual-batch postprocess.</li>
  <li>Host capture now stamps engine-truth <code class="language-plaintext highlighter-rouge">scene_start_frame</code> / <code class="language-plaintext highlighter-rouge">scene_end_frame</code> from the executable itself.</li>
</ul>

<p>Known issues</p>
<ul>
  <li>Full-frame PS1 telemetry decode is still too expensive for interactive per-frame validation unless <code class="language-plaintext highlighter-rouge">--fast-baseline</code> is used.</li>
  <li>First live validation scene (<code class="language-plaintext highlighter-rouge">BUILDING 1</code>) still does not produce a positive PS1-to-host alignment yet.</li>
  <li>Old Dockerized regtest outputs can leave root-owned files in result directories; reruns should use fresh output dirs or the harness should isolate/chown them.</li>
  <li>The sequence comparator is still too slow once the PS1 side has thousands of candidate frames; entry-anchor search needs one more optimization pass before it becomes a comfortable interactive loop.</li>
</ul>

<p>Validated so far</p>
<ul>
  <li>Canonical host references are present and organized.</li>
  <li>Shell/python syntax for the updated harness scripts is clean.</li>
  <li>The PS1 validation lane now reaches the sequence comparator instead of failing earlier on missing build directories or missing result metadata.</li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 1</code> host reference starts at real scene content; <code class="language-plaintext highlighter-rouge">visual_detect.py</code> is unreliable on host BMP frames and must not be used as the host entry oracle.</li>
  <li><code class="language-plaintext highlighter-rouge">BUILDING 1</code> PS1 <code class="language-plaintext highlighter-rouge">story single 10</code> remains in title/ocean flow for a long time; the first scene-like sampled frame in a 4200-frame probe is <code class="language-plaintext highlighter-rouge">frame_03660.png</code>.</li>
  <li>That same PS1 probe still does not match the canonical <code class="language-plaintext highlighter-rouge">BUILDING 1</code> host baseline by <code class="language-plaintext highlighter-rouge">frame_04200</code>; this is a real content mismatch, not just a missing-frames plumbing failure.</li>
  <li>A short <code class="language-plaintext highlighter-rouge">story direct 10</code> PS1 validation run still spends hundreds of frames in BIOS startup, so even the direct lane needs a larger boot budget than 600 frames.</li>
  <li><code class="language-plaintext highlighter-rouge">story direct</code> is not an apples-to-apples baseline route. <code class="language-plaintext highlighter-rouge">story single</code> preserves the normal startup flow, while <code class="language-plaintext highlighter-rouge">story direct</code> bypasses it and adds a 600-frame hold tail by design.</li>
  <li>Fresh host <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> capture with engine markers reports <code class="language-plaintext highlighter-rouge">scene_start_frame = 7</code>, <code class="language-plaintext highlighter-rouge">scene_end_frame = 328</code>, <code class="language-plaintext highlighter-rouge">frames_captured = 328</code>. So host scene boundaries no longer need to be guessed visually.</li>
  <li>A cheap sampled PS1 scan for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> shows first non-title content around frame <code class="language-plaintext highlighter-rouge">2700</code>, but it is <code class="language-plaintext highlighter-rouge">ocean</code>, not the host activity scene.</li>
  <li>The comparator now fails fast on missing anchors instead of hanging for minutes; remaining speed pain is reduced to long known-fail searches rather than general unusability.</li>
  <li>Fresh host-vs-host <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> comparison is an exact <code class="language-plaintext highlighter-rouge">MATCH</code> with <code class="language-plaintext highlighter-rouge">frame_offset = 0</code>, <code class="language-plaintext highlighter-rouge">common_frame_count = 328</code>, and <code class="language-plaintext highlighter-rouge">average_palette_index_diff_pixels = 0.0</code>. The comparator now has a real positive-control path.</li>
  <li>Long sampled PS1 run for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> reaches <code class="language-plaintext highlighter-rouge">ocean</code> around frame <code class="language-plaintext highlighter-rouge">2700</code> and does not reach any host-matchable <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> scene even after entering <code class="language-plaintext highlighter-rouge">island</code> content around frame <code class="language-plaintext highlighter-rouge">6300</code>.</li>
  <li>Late-window compare against the long sampled PS1 run still reports <code class="language-plaintext highlighter-rouge">missing verified scene_entry anchor</code>, so the PS1 side is eventually showing the wrong island scene, not merely arriving late to the right one.</li>
</ul>

<p>Next steps</p>
<ol>
  <li>Trace why PS1 <code class="language-plaintext highlighter-rouge">story single</code> boots are entering the wrong island scene for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> as the first live PS1 route/debug target until the requested scene is actually reached.</li>
  <li>After that, revalidate <code class="language-plaintext highlighter-rouge">BUILDING 1</code> on the same harness and continue scene-by-scene.</li>
  <li>Keep the harness stable and avoid further heuristic scene-entry work unless a new gap appears.</li>
</ol>

<p>Progress update</p>
<ul>
  <li>Fast direct validation lane shows PS1 still in BIOS/logo at ~frame 1500 and plain ocean at frames 2000-2400 for <code class="language-plaintext highlighter-rouge">story direct 10</code>.</li>
  <li>This strongly suggests a real PS1 scene-routing mismatch for <code class="language-plaintext highlighter-rouge">BUILDING 1</code>, not just harness timing error.</li>
  <li>The remaining harness gap is no longer host scene detection; it is PS1 entry-window search and comparator speed over long PS1 captures.</li>
  <li>With host scene markers in place, the next blocker is no longer “when does host scene begin?” but “why does PS1 <code class="language-plaintext highlighter-rouge">story single</code> land in ocean content for scenes like <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>?”</li>
  <li>Next debug target after the speed pass: trace PS1 story-route handling for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> / <code class="language-plaintext highlighter-rouge">BUILDING 1</code> and fix that before deeper pixel work.</li>
  <li>Comparator confidence is now materially higher: host capture + host reference can round-trip to an exact match, so the remaining failures are on the PS1 path or in PS1-to-host alignment policy, not in basic compare mechanics.</li>
  <li>Practical conclusion: the harness is now good enough for scene debugging. The current blocker is a real PS1 route/content bug, first visible on <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>.</li>
  <li>Debugging moved into <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> as the first real PS1 scene bug target.</li>
  <li><code class="language-plaintext highlighter-rouge">story single 0</code>, <code class="language-plaintext highlighter-rouge">story scene 0</code>, and exact <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> all hit the same wrong ocean window by frame <code class="language-plaintext highlighter-rouge">4200</code>, so the failure is not scene-index mapping.</li>
  <li>Narrowing the old random empty-launch ADS retry from all families down to <code class="language-plaintext highlighter-rouge">BUILDING.ADS</code> did not change <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> behavior.</li>
  <li>Added robust current-ADS family/tag markers into the PS1 scene-marker strip so sampled frames can now tell us exactly what PS1 thinks is active during the bad window.</li>
  <li>Added current ADS family/tag telemetry to the PS1 scene-marker strip.</li>
  <li>First marker probe on <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> shows <code class="language-plaintext highlighter-rouge">current_ads_family=0</code> / <code class="language-plaintext highlighter-rouge">current_ads_tag=0</code> at frames <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code>, with <code class="language-plaintext highlighter-rouge">ADD_SCENE</code> seen but <code class="language-plaintext highlighter-rouge">launched=false</code>.</li>
  <li>Long exact boot <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> still does not reach host <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>.</li>
  <li>Sampled frames <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code> remain in failed-launch ocean (<code class="language-plaintext highlighter-rouge">launched=false</code>, <code class="language-plaintext highlighter-rouge">family=0</code>, <code class="language-plaintext highlighter-rouge">tag=0</code>).</li>
  <li>By frames <code class="language-plaintext highlighter-rouge">6300</code> and <code class="language-plaintext highlighter-rouge">8700</code>, PS1 has transitioned into live island content with Johnny/raft/palm that visual detection classifies as FISHING-like, not ACTIVITY.</li>
  <li>So <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> on PS1 is not merely late; it eventually turns into the wrong scene family/content.</li>
  <li>Added requested-scene and boot-pending markers to the robust PS1 strip to separate story selection bugs from ADS launch bugs.</li>
  <li>Fixed <code class="language-plaintext highlighter-rouge">regtest-scene.sh</code> scene-label parsing so scratch labels with spaces no longer corrupt <code class="language-plaintext highlighter-rouge">ADS_NAME</code> / <code class="language-plaintext highlighter-rouge">SCENE_TAG</code> or break result summary generation.</li>
  <li>Switched requested/current ADS markers to white bit-cells for more robust decode.</li>
  <li>On the <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> bitmark probe, <code class="language-plaintext highlighter-rouge">boot_pending</code> stays asserted at frames <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code> while the run remains in failed-launch ocean.</li>
  <li>That rules out early override consumption as the main remaining bug; the failure is downstream of story selection, inside PS1 ADS/TTM launch state.</li>
  <li>Enlarged the isolated black telemetry backdrop for requested/current ADS bit-cell rows so lower rows no longer bleed against live scene pixels.</li>
  <li>Added <code class="language-plaintext highlighter-rouge">ps1AdsDbgZeroIpLaunches</code> telemetry to distinguish tag-resolution failure from later immediate thread death.</li>
  <li>Added <code class="language-plaintext highlighter-rouge">zero_ip_launch</code> to the robust scene-marker decoder and reran an exact <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> PS1 probe on the rebuilt image.</li>
  <li>Fresh decode at frames <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code> shows:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">launched=false</code></li>
      <li><code class="language-plaintext highlighter-rouge">add_scene=true</code></li>
      <li><code class="language-plaintext highlighter-rouge">tag_hit=true</code></li>
      <li><code class="language-plaintext highlighter-rouge">bmp_fail=true</code></li>
      <li><code class="language-plaintext highlighter-rouge">sprite_seen=true</code></li>
      <li><code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
    </ul>
  </li>
  <li>However, authored bytecode inspection shows <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS tag 1</code> starts with valid nonzero launches:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">slot 1 / GJDIVE.TTM / tag 12 / ip 42</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 1 / GJDIVE.TTM / tag 13 / ip 126</code></li>
    </ul>
  </li>
  <li>That means the later <code class="language-plaintext highlighter-rouge">zero_ip_launch</code> signal in the ocean window is a downstream symptom, not proof that the initial <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> launch target is bad.</li>
  <li>The next debug question is no longer “does <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> target slot 0?” It is “why do the valid initial <code class="language-plaintext highlighter-rouge">GJDIVE</code> scene launches fail to survive into a live PS1 scene before the run drifts into unrelated content?”</li>
  <li>Added sticky robust-strip bits for the authored <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> startup launches:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">activity_init12_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init13_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init12_ended</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init13_ended</code></li>
    </ul>
  </li>
  <li>Fresh exact PS1 probe on the rebuilt image still reproduces the same failure:
    <ul>
      <li>title/transition noise around <code class="language-plaintext highlighter-rouge">300-600</code></li>
      <li>black by <code class="language-plaintext highlighter-rouge">900-1200</code></li>
      <li>wrong-ocean failure window by <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code></li>
    </ul>
  </li>
  <li>In that bad ocean window, the new strip bits indicate PS1 is at least touching the authored <code class="language-plaintext highlighter-rouge">GJDIVE</code> startup path rather than a totally unrelated scene family from the outset:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">activity_init12_launched = true</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init13_launched = false</code> on the sampled decode</li>
      <li><code class="language-plaintext highlighter-rouge">activity_init12_ended = false</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init13_ended = false</code></li>
    </ul>
  </li>
  <li>The decode is still too coarse to treat the exact <code class="language-plaintext highlighter-rouge">12</code> vs <code class="language-plaintext highlighter-rouge">13</code> split as final truth, but it is enough to shift the hypothesis again:
    <ul>
      <li>PS1 is not simply ignoring <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
      <li>it is entering the <code class="language-plaintext highlighter-rouge">ACTIVITY</code> launch path, then collapsing before a stable live scene forms</li>
    </ul>
  </li>
  <li>Next target: instrument per-thread startup failure more directly around <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code> tags <code class="language-plaintext highlighter-rouge">12</code> and <code class="language-plaintext highlighter-rouge">13</code> so we can tell whether one of them is never started, immediately terminates, or is superseded by a later bad launch.</li>
  <li>Added live robust-strip bits for:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">activity_init12_running</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_init13_running</code></li>
    </ul>
  </li>
  <li>Fresh exact probe with those rows shows:
    <ul>
      <li>at <code class="language-plaintext highlighter-rouge">300</code> and <code class="language-plaintext highlighter-rouge">600</code>: both <code class="language-plaintext highlighter-rouge">12</code> and <code class="language-plaintext highlighter-rouge">13</code> appear as launched/running during startup/title-path noise</li>
      <li>at <code class="language-plaintext highlighter-rouge">2700</code> and <code class="language-plaintext highlighter-rouge">4200</code>: neither <code class="language-plaintext highlighter-rouge">12</code> nor <code class="language-plaintext highlighter-rouge">13</code> is running anymore</li>
      <li>the bad ocean window still shows:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">add_scene=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_hit=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_seen=true</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>So the current read is stronger now:
    <ul>
      <li>the authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> startup TTM threads do exist early</li>
      <li>they are gone long before the later wrong-ocean window</li>
      <li>the bad ocean state is a later broken relaunch/fallback condition, not the original <code class="language-plaintext highlighter-rouge">GJDIVE 12/13</code> scene simply staying alive in the wrong form</li>
    </ul>
  </li>
  <li>Next target: identify what later <code class="language-plaintext highlighter-rouge">ADD_SCENE</code> / relaunch path is taking over after the initial <code class="language-plaintext highlighter-rouge">ACTIVITY</code> startup threads disappear.</li>
  <li>Added sticky robust-strip bits for later authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> relaunch edges:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">activity_tag7_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_tag8_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_tag9_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_tag14_launched</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched</code></li>
    </ul>
  </li>
  <li>Fresh exact <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> probe to <code class="language-plaintext highlighter-rouge">4200</code> with those rows:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-relaunchtrace/result.json</code></li>
      <li>At <code class="language-plaintext highlighter-rouge">frame_02700</code> the bad ocean window still shows:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">add_scene=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_hit=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li>none of <code class="language-plaintext highlighter-rouge">activity_tag7/8/9/11/14</code> or <code class="language-plaintext highlighter-rouge">activity_slot2_tag2</code> are set</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Long exact probe to <code class="language-plaintext highlighter-rouge">9000</code> with <code class="language-plaintext highlighter-rouge">900</code>-frame sampling:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-relaunchtrace-long/result.json</code></li>
      <li>By <code class="language-plaintext highlighter-rouge">frame_06300</code> and still at <code class="language-plaintext highlighter-rouge">frame_08100</code>, the later wrong live scene shows:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_miss=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag7/8/9/14=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_init12_running=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_init13_running=true</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>This narrows the active PS1 bug again:
    <ul>
      <li>the bad ocean window at <code class="language-plaintext highlighter-rouge">2700-4200</code> happens before the later authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> relaunch chain is touched</li>
      <li>by <code class="language-plaintext highlighter-rouge">6300+</code>, the authored relaunch chain has advanced at least as far as <code class="language-plaintext highlighter-rouge">ACTIVITY tag 11 -&gt; slot 2 tag 2</code></li>
      <li>the eventual wrong live scene therefore appears during or after that later authored relaunch handoff, not during initial story selection and not during the first <code class="language-plaintext highlighter-rouge">GJDIVE 12/13</code> startup launches</li>
    </ul>
  </li>
  <li>Next target: instrument the transition from <code class="language-plaintext highlighter-rouge">ACTIVITY tag 11 / slot2:2</code> into the later <code class="language-plaintext highlighter-rouge">zero_ip_launch</code> live scene takeover, because that is now the narrowest unexplained edge in the failure.</li>
  <li>Added later handoff liveness/end rows:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">activity_tag11_running</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_running</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_tag11_ended</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_ended</code></li>
    </ul>
  </li>
  <li>Fresh rebuilt long probe:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-handofftrace-long/result.json</code></li>
    </ul>
  </li>
  <li>Decoded checkpoints:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_02700</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">add_scene=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_hit=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_running=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_running=false</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_06300</code> and <code class="language-plaintext highlighter-rouge">frame_08100</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_miss=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_running=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_running=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_ended=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_ended=false</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>That tightens the read further:
    <ul>
      <li>by the later wrong-scene window, the authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> relaunch chain has definitely touched <code class="language-plaintext highlighter-rouge">tag11 -&gt; slot2:2</code></li>
      <li>but those threads are no longer running there</li>
      <li>and they are not being observed to terminate cleanly through <code class="language-plaintext highlighter-rouge">adsStopScene()</code></li>
      <li>the later wrong live scene therefore looks like a takeover after launched threads disappear without a normal observed stop, while <code class="language-plaintext highlighter-rouge">zero_ip_launch</code> continues to accumulate</li>
    </ul>
  </li>
  <li>Next target: instrument where launched <code class="language-plaintext highlighter-rouge">ACTIVITY tag11 / slot2:2</code> threads are being invalidated or superseded without a normal stop, and connect that to the continuing <code class="language-plaintext highlighter-rouge">zero_ip_launch</code> takeover path.</li>
  <li>Dumped the authored reachable <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> chunk graph with the real parser:
    <ul>
      <li>entry chunk bookmarks <code class="language-plaintext highlighter-rouge">1:13</code>, <code class="language-plaintext highlighter-rouge">1:8</code>, <code class="language-plaintext highlighter-rouge">1:7</code>, <code class="language-plaintext highlighter-rouge">1:9</code>, <code class="language-plaintext highlighter-rouge">1:14</code>, <code class="language-plaintext highlighter-rouge">2:2</code></li>
      <li>direct chunk contents confirm the authored loop:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">1:13</code> randomly launches <code class="language-plaintext highlighter-rouge">1:8</code>, <code class="language-plaintext highlighter-rouge">1:7</code>, <code class="language-plaintext highlighter-rouge">1:9</code></li>
          <li><code class="language-plaintext highlighter-rouge">1:8/7/9</code> can launch <code class="language-plaintext highlighter-rouge">1:14</code></li>
          <li><code class="language-plaintext highlighter-rouge">1:14</code> launches <code class="language-plaintext highlighter-rouge">2:2</code></li>
          <li><code class="language-plaintext highlighter-rouge">2:2</code> relaunches <code class="language-plaintext highlighter-rouge">1:13</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Ran a denser transition probe:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-transitiontrace/result.json</code></li>
    </ul>
  </li>
  <li>Decoded transition band:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_05400</code> through <code class="language-plaintext highlighter-rouge">frame_06000</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">add_scene=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_hit=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched=false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched=false</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag_miss=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">zero_ip_launch=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_launched=true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_launched=true</code></li>
          <li>neither is running anymore by the time of the sample</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>That narrows the failure boundary again:
    <ul>
      <li>the later authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> relaunch loop first becomes visible between <code class="language-plaintext highlighter-rouge">6000</code> and <code class="language-plaintext highlighter-rouge">6150</code></li>
      <li><code class="language-plaintext highlighter-rouge">zero_ip_launch</code> is already active well before that switch</li>
      <li>so the zero-IP takeover is not a consequence of the <code class="language-plaintext highlighter-rouge">tag11/slot2:2</code> handoff; it predates it and persists through it</li>
    </ul>
  </li>
  <li>Next target: instrument the exact launch happening in the <code class="language-plaintext highlighter-rouge">6000-6150</code> window and record the slot/tag/IP of the first live scene that appears once <code class="language-plaintext highlighter-rouge">launched=true</code> flips on.</li>
  <li>Found a real decoder bug in <code class="language-plaintext highlighter-rouge">scripts/decode-ps1-bars.py</code>: scene-marker rows were being scaled against <code class="language-plaintext highlighter-rouge">448</code> lines instead of native <code class="language-plaintext highlighter-rouge">480</code>, which vertically misaligned the robust strip decode on PS1 screenshots.</li>
  <li>After fixing the scene-marker scale to <code class="language-plaintext highlighter-rouge">img.height / 480.0</code>, the later handoff frame reads materially differently and more plausibly:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code> now decodes as:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">launched = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_tag11_running = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_slot2_tag2_running = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_slot2 = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_ip_zero = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_ip_nonzero = false</code></li>
        </ul>
      </li>
      <li>the live-thread tag one-hots for <code class="language-plaintext highlighter-rouge">2</code>, <code class="language-plaintext highlighter-rouge">11</code>, and <code class="language-plaintext highlighter-rouge">13</code> are all false in that same frame</li>
    </ul>
  </li>
  <li>Current read after the decoder fix:
    <ul>
      <li>the first visible live thread at the handoff is a <code class="language-plaintext highlighter-rouge">slot 2</code> thread with <code class="language-plaintext highlighter-rouge">ip == 0</code></li>
      <li>it is not one of the expected tag one-hots currently instrumented (<code class="language-plaintext highlighter-rouge">2</code>, <code class="language-plaintext highlighter-rouge">11</code>, <code class="language-plaintext highlighter-rouge">13</code>)</li>
      <li>meanwhile the authored <code class="language-plaintext highlighter-rouge">ACTIVITY</code> handoff threads (<code class="language-plaintext highlighter-rouge">tag11</code> and <code class="language-plaintext highlighter-rouge">slot2:2</code>) are still marked as running in the same frame</li>
    </ul>
  </li>
  <li>This narrows the PS1 bug again:
    <ul>
      <li>the bad takeover at the handoff is now consistent with a live <code class="language-plaintext highlighter-rouge">slot 2</code> zero-IP thread preempting the intended authored loop</li>
      <li>the next probe should identify that live <code class="language-plaintext highlighter-rouge">slot 2</code> tag exactly, rather than continuing to infer from the old compact bitcells or noisy emulator logs</li>
    </ul>
  </li>
</ul>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#2026-03-28-1259-pdt" id="markdown-toc-2026-03-28-1259-pdt">2026-03-28 12:59 PDT</a></li>
  <li><a href="#2026-03-28-1430-pdt" id="markdown-toc-2026-03-28-1430-pdt">2026-03-28 14:30 PDT</a></li>
  <li><a href="#2026-03-28-1302-pdt" id="markdown-toc-2026-03-28-1302-pdt">2026-03-28 13:02 PDT</a></li>
  <li><a href="#2026-03-28-1331-pdt" id="markdown-toc-2026-03-28-1331-pdt">2026-03-28 13:31 PDT</a></li>
  <li><a href="#2026-03-28-1340-pdt" id="markdown-toc-2026-03-28-1340-pdt">2026-03-28 13:40 PDT</a></li>
  <li><a href="#2026-03-28-1346-pdt" id="markdown-toc-2026-03-28-1346-pdt">2026-03-28 13:46 PDT</a></li>
  <li><a href="#2026-03-28-1351-pdt" id="markdown-toc-2026-03-28-1351-pdt">2026-03-28 13:51 PDT</a></li>
  <li><a href="#2026-03-28-1355-pdt" id="markdown-toc-2026-03-28-1355-pdt">2026-03-28 13:55 PDT</a></li>
  <li><a href="#2026-03-28-1359-pdt" id="markdown-toc-2026-03-28-1359-pdt">2026-03-28 13:59 PDT</a></li>
  <li><a href="#2026-03-28-1403-pdt" id="markdown-toc-2026-03-28-1403-pdt">2026-03-28 14:03 PDT</a></li>
  <li><a href="#2026-03-28-1405-pdt" id="markdown-toc-2026-03-28-1405-pdt">2026-03-28 14:05 PDT</a></li>
  <li><a href="#2026-03-28-1411-pdt" id="markdown-toc-2026-03-28-1411-pdt">2026-03-28 14:11 PDT</a></li>
  <li><a href="#2026-03-28-1448-pdt" id="markdown-toc-2026-03-28-1448-pdt">2026-03-28 14:48 PDT</a></li>
  <li><a href="#2026-03-28-1503-pdt" id="markdown-toc-2026-03-28-1503-pdt">2026-03-28 15:03 PDT</a></li>
  <li><a href="#2026-03-28-1513-pdt" id="markdown-toc-2026-03-28-1513-pdt">2026-03-28 15:13 PDT</a></li>
  <li><a href="#2026-03-28-1534-pdt" id="markdown-toc-2026-03-28-1534-pdt">2026-03-28 15:34 PDT</a></li>
  <li><a href="#2026-03-28-1538-pdt" id="markdown-toc-2026-03-28-1538-pdt">2026-03-28 15:38 PDT</a></li>
  <li><a href="#2026-03-28-1539-pdt" id="markdown-toc-2026-03-28-1539-pdt">2026-03-28 15:39 PDT</a></li>
  <li><a href="#2026-03-28-1608-pdt" id="markdown-toc-2026-03-28-1608-pdt">2026-03-28 16:08 PDT</a></li>
  <li><a href="#2026-03-28-1618-pdt" id="markdown-toc-2026-03-28-1618-pdt">2026-03-28 16:18 PDT</a></li>
  <li><a href="#2026-03-28-1621-pdt" id="markdown-toc-2026-03-28-1621-pdt">2026-03-28 16:21 PDT</a></li>
  <li><a href="#2026-03-28-1630-pdt" id="markdown-toc-2026-03-28-1630-pdt">2026-03-28 16:30 PDT</a></li>
  <li><a href="#2026-03-28-1635-pdt" id="markdown-toc-2026-03-28-1635-pdt">2026-03-28 16:35 PDT</a></li>
  <li><a href="#2026-03-28-1640-pdt" id="markdown-toc-2026-03-28-1640-pdt">2026-03-28 16:40 PDT</a></li>
  <li><a href="#2026-03-28-1645-pdt" id="markdown-toc-2026-03-28-1645-pdt">2026-03-28 16:45 PDT</a></li>
  <li><a href="#2026-03-28-1650-pdt" id="markdown-toc-2026-03-28-1650-pdt">2026-03-28 16:50 PDT</a></li>
  <li><a href="#2026-03-28-1659-pdt" id="markdown-toc-2026-03-28-1659-pdt">2026-03-28 16:59 PDT</a></li>
  <li><a href="#2026-03-28-1702-pdt" id="markdown-toc-2026-03-28-1702-pdt">2026-03-28 17:02 PDT</a></li>
  <li><a href="#2026-03-28-1706-pdt" id="markdown-toc-2026-03-28-1706-pdt">2026-03-28 17:06 PDT</a></li>
  <li><a href="#2026-03-28-1709-pdt" id="markdown-toc-2026-03-28-1709-pdt">2026-03-28 17:09 PDT</a></li>
  <li><a href="#2026-03-28-1715-pdt" id="markdown-toc-2026-03-28-1715-pdt">2026-03-28 17:15 PDT</a></li>
  <li><a href="#2026-03-28-1717-pdt" id="markdown-toc-2026-03-28-1717-pdt">2026-03-28 17:17 PDT</a></li>
  <li><a href="#2026-03-28-1720-pdt" id="markdown-toc-2026-03-28-1720-pdt">2026-03-28 17:20 PDT</a></li>
  <li><a href="#2026-03-28-1722-pdt" id="markdown-toc-2026-03-28-1722-pdt">2026-03-28 17:22 PDT</a></li>
  <li><a href="#2026-03-28-1723-pdt" id="markdown-toc-2026-03-28-1723-pdt">2026-03-28 17:23 PDT</a></li>
  <li><a href="#2026-03-28-1729-pdt" id="markdown-toc-2026-03-28-1729-pdt">2026-03-28 17:29 PDT</a></li>
  <li><a href="#2026-03-28-1736-pdt" id="markdown-toc-2026-03-28-1736-pdt">2026-03-28 17:36 PDT</a></li>
  <li><a href="#2026-03-28-1744-pdt" id="markdown-toc-2026-03-28-1744-pdt">2026-03-28 17:44 PDT</a></li>
  <li><a href="#2026-03-28-1751-pdt" id="markdown-toc-2026-03-28-1751-pdt">2026-03-28 17:51 PDT</a></li>
  <li><a href="#2026-03-28-1757-pdt" id="markdown-toc-2026-03-28-1757-pdt">2026-03-28 17:57 PDT</a></li>
  <li><a href="#2026-03-28-1801-pdt" id="markdown-toc-2026-03-28-1801-pdt">2026-03-28 18:01 PDT</a></li>
  <li><a href="#2026-03-28-1806-pdt" id="markdown-toc-2026-03-28-1806-pdt">2026-03-28 18:06 PDT</a></li>
  <li><a href="#2026-03-28-1811-pdt" id="markdown-toc-2026-03-28-1811-pdt">2026-03-28 18:11 PDT</a></li>
  <li><a href="#2026-03-28-1815-pdt" id="markdown-toc-2026-03-28-1815-pdt">2026-03-28 18:15 PDT</a></li>
  <li><a href="#2026-03-28-1828-pdt" id="markdown-toc-2026-03-28-1828-pdt">2026-03-28 18:28 PDT</a></li>
  <li><a href="#1832-pdt---post-intro-scrub-proof-changes-failure-to-stable-title-not-black-or-ocean" id="markdown-toc-1832-pdt---post-intro-scrub-proof-changes-failure-to-stable-title-not-black-or-ocean">18:32 PDT - Post-intro scrub proof changes failure to stable title, not black or ocean</a></li>
  <li><a href="#1834-pdt---replacing-first-exact-activity-prepare-with-adsnoisland-yields-black-not-ocean" id="markdown-toc-1834-pdt---replacing-first-exact-activity-prepare-with-adsnoisland-yields-black-not-ocean">18:34 PDT - Replacing first exact ACTIVITY prepare with <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> yields black, not ocean</a></li>
  <li><a href="#1840-pdt---skipping-adsprimesceneresources-does-not-change-the-stable-ocean-failure" id="markdown-toc-1840-pdt---skipping-adsprimesceneresources-does-not-change-the-stable-ocean-failure">18:40 PDT - Skipping <code class="language-plaintext highlighter-rouge">adsPrimeSceneResources()</code> does not change the stable ocean failure</a></li>
  <li><a href="#1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910" id="markdown-toc-1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910">18:45 PDT - Skipping exact scene island-state calculation perturbs the lead-in, but ocean still settles by <code class="language-plaintext highlighter-rouge">04910</code></a></li>
  <li><a href="#0216-pdt---keeping-the-first-small-pack-header-buffer-alive-did-not-move-activity-1" id="markdown-toc-0216-pdt---keeping-the-first-small-pack-header-buffer-alive-did-not-move-activity-1">02:16 PDT - Keeping the first small pack-header buffer alive did not move <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0218-pdt---copying-cdlfile-immediately-after-cdsearchfile-moved-activity-1-to-the-third-branch" id="markdown-toc-0218-pdt---copying-cdlfile-immediately-after-cdsearchfile-moved-activity-1-to-the-third-branch">02:18 PDT - Copying <code class="language-plaintext highlighter-rouge">CdlFILE</code> immediately after <code class="language-plaintext highlighter-rouge">CdSearchFile</code> moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> to the third branch</a></li>
  <li><a href="#0221-pdt---moving-cdlfile-fully-off-the-local-stack-also-moved-activity-1" id="markdown-toc-0221-pdt---moving-cdlfile-fully-off-the-local-stack-also-moved-activity-1">02:21 PDT - Moving <code class="language-plaintext highlighter-rouge">CdlFILE</code> fully off the local stack also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0224-pdt---moving-cdpath-off-the-local-stack-also-moved-activity-1" id="markdown-toc-0224-pdt---moving-cdpath-off-the-local-stack-also-moved-activity-1">02:24 PDT - Moving <code class="language-plaintext highlighter-rouge">cdPath</code> off the local stack also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0227-pdt---simple-local-padding-between-cdpath-and-cdfile-did-not-move-activity-1" id="markdown-toc-0227-pdt---simple-local-padding-between-cdpath-and-cdfile-did-not-move-activity-1">02:27 PDT - Simple local padding between <code class="language-plaintext highlighter-rouge">cdPath</code> and <code class="language-plaintext highlighter-rouge">cdfile</code> did not move <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0231-pdt---swapping-local-order-of-cdfile-and-cdpath-moved-activity-1" id="markdown-toc-0231-pdt---swapping-local-order-of-cdfile-and-cdpath-moved-activity-1">02:31 PDT - Swapping local order of <code class="language-plaintext highlighter-rouge">cdfile</code> and <code class="language-plaintext highlighter-rouge">cdPath</code> moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0236-pdt---a-tiny-helper-around-cdsearchfile-also-moved-activity-1" id="markdown-toc-0236-pdt---a-tiny-helper-around-cdsearchfile-also-moved-activity-1">02:36 PDT - A tiny helper around <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0155-pdt---evaluating-the-old-activitypak-conditional-alone-is-enough-to-hold-the-improved-branch" id="markdown-toc-0155-pdt---evaluating-the-old-activitypak-conditional-alone-is-enough-to-hold-the-improved-branch">01:55 PDT - Evaluating the old <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> conditional alone is enough to hold the improved branch</a></li>
  <li><a href="#0200-pdt---pure-local-stack-padding-moves-the-run-to-a-third-stable-branch" id="markdown-toc-0200-pdt---pure-local-stack-padding-moves-the-run-to-a-third-stable-branch">02:00 PDT - Pure local stack padding moves the run to a third stable branch</a></li>
  <li><a href="#0205-pdt---basic-decoded-field-bounds-hardening-does-not-move-the-run" id="markdown-toc-0205-pdt---basic-decoded-field-bounds-hardening-does-not-move-the-run">02:05 PDT - Basic decoded-field bounds hardening does not move the run</a></li>
  <li><a href="#0124-pdt---root-file-lookup-is-healthy-before-activitypak-search-but-not-after-it" id="markdown-toc-0124-pdt---root-file-lookup-is-healthy-before-activitypak-search-but-not-after-it">01:24 PDT - Root-file lookup is healthy before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search, but not after it</a></li>
  <li><a href="#0131-pdt---another-pack-file-resolves-correctly-before-activitypak-search" id="markdown-toc-0131-pdt---another-pack-file-resolves-correctly-before-activitypak-search">01:31 PDT - Another pack file resolves correctly before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search</a></li>
  <li><a href="#0132-pdt---literal-packsactivitypak1-lookup-fails-too" id="markdown-toc-0132-pdt---literal-packsactivitypak1-lookup-fails-too">01:32 PDT - Literal <code class="language-plaintext highlighter-rouge">\\PACKS\\ACTIVITY.PAK;1</code> lookup fails too</a></li>
  <li><a href="#0135-pdt---the-image-has-exactly-one-activitypak-record-and-repeated-literal-lookups-stay-on-the-improved-branch" id="markdown-toc-0135-pdt---the-image-has-exactly-one-activitypak-record-and-repeated-literal-lookups-stay-on-the-improved-branch">01:35 PDT - The image has exactly one <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> record, and repeated literal lookups stay on the improved branch</a></li>
  <li><a href="#0140-pdt---a-single-literal-activitypak-prelookup-is-not-enough" id="markdown-toc-0140-pdt---a-single-literal-activitypak-prelookup-is-not-enough">01:40 PDT - A single literal <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> prelookup is not enough</a></li>
  <li><a href="#0145-pdt---reusing-a-known-good-literal-cdlfile-still-does-not-stabilize-the-normal-path" id="markdown-toc-0145-pdt---reusing-a-known-good-literal-cdlfile-still-does-not-stabilize-the-normal-path">01:45 PDT - Reusing a known-good literal <code class="language-plaintext highlighter-rouge">CdlFILE</code> still does not stabilize the normal path</a></li>
  <li><a href="#0150-pdt---double-literal-lookup-without-the-old-conditional-still-falls-back" id="markdown-toc-0150-pdt---double-literal-lookup-without-the-old-conditional-still-falls-back">01:50 PDT - Double literal lookup without the old conditional still falls back</a></li>
  <li><a href="#0036-pdt---activity-ads-metadata-is-already-wrong-immediately-after-ps1_loadadsdata" id="markdown-toc-0036-pdt---activity-ads-metadata-is-already-wrong-immediately-after-ps1_loadadsdata">00:36 PDT - ACTIVITY ADS metadata is already wrong immediately after <code class="language-plaintext highlighter-rouge">ps1_loadAdsData()</code></a></li>
  <li><a href="#2355-pdt---older-6dae8410-base-is-not-a-trustworthy-post-boot-ps1-scene-debug-target" id="markdown-toc-2355-pdt---older-6dae8410-base-is-not-a-trustworthy-post-boot-ps1-scene-debug-target">23:55 PDT - Older <code class="language-plaintext highlighter-rouge">6dae8410</code> base is not a trustworthy post-boot PS1 scene-debug target</a></li>
  <li><a href="#0005-pdt---returned-to-current-head-activity-1-remains-the-live-scene-target" id="markdown-toc-0005-pdt---returned-to-current-head-activity-1-remains-the-live-scene-target">00:05 PDT - Returned to current <code class="language-plaintext highlighter-rouge">HEAD</code>; <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> remains the live scene target</a></li>
  <li><a href="#0012-pdt---current-activity-1-still-crosses-the-old-pre-chunk-slot-1-corruption-seam" id="markdown-toc-0012-pdt---current-activity-1-still-crosses-the-old-pre-chunk-slot-1-corruption-seam">00:12 PDT - Current <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> still crosses the old pre-chunk slot-1 corruption seam</a></li>
  <li><a href="#0018-pdt---activity-1-slot-1-corruption-is-already-present-by-the-time-adsload-returns" id="markdown-toc-0018-pdt---activity-1-slot-1-corruption-is-already-present-by-the-time-adsload-returns">00:18 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> slot-1 corruption is already present by the time <code class="language-plaintext highlighter-rouge">adsLoad()</code> returns</a></li>
  <li><a href="#0025-pdt---adsload-runtime-tag-table-no-longer-preserves-authored-activity-startup-tag-12---176" id="markdown-toc-0025-pdt---adsload-runtime-tag-table-no-longer-preserves-authored-activity-startup-tag-12---176">00:25 PDT - <code class="language-plaintext highlighter-rouge">adsLoad()</code> runtime tag table no longer preserves authored ACTIVITY startup tag <code class="language-plaintext highlighter-rouge">12 -&gt; 176</code></a></li>
  <li><a href="#0031-pdt---activityads-parse-time-metadata-is-still-correct-corruption-happens-later" id="markdown-toc-0031-pdt---activityads-parse-time-metadata-is-still-correct-corruption-happens-later">00:31 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> parse-time metadata is still correct; corruption happens later</a></li>
  <li><a href="#0039-pdt---the-bad-size-comes-from-the-pilot-pack-ads-load-path-not-from-authored-pack-content" id="markdown-toc-0039-pdt---the-bad-size-comes-from-the-pilot-pack-ads-load-path-not-from-authored-pack-content">00:39 PDT - The bad size comes from the pilot-pack ADS load path, not from authored pack content</a></li>
  <li><a href="#0045-pdt---runtime-activitypak-index-handling-is-still-the-live-seam" id="markdown-toc-0045-pdt---runtime-activitypak-index-handling-is-still-the-live-seam">00:45 PDT - Runtime <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> index handling is still the live seam</a></li>
  <li><a href="#0050-pdt---ps1pilotactivepackentries-is-already-wrong-immediately-after-pack-activation" id="markdown-toc-0050-pdt---ps1pilotactivepackentries-is-already-wrong-immediately-after-pack-activation">00:50 PDT - <code class="language-plaintext highlighter-rouge">ps1PilotActivePack.entries</code> is already wrong immediately after pack activation</a></li>
  <li><a href="#0055-pdt---cold-resetting-activeprefetch-packs-does-not-recover-activity-1" id="markdown-toc-0055-pdt---cold-resetting-activeprefetch-packs-does-not-recover-activity-1">00:55 PDT - Cold-resetting active/prefetch packs does not recover <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#0100-pdt---fresh-activitypak-header-parse-is-already-wrong-on-the-first-20-byte-read" id="markdown-toc-0100-pdt---fresh-activitypak-header-parse-is-already-wrong-on-the-first-20-byte-read">01:00 PDT - Fresh <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header parse is already wrong on the first 20-byte read</a></li>
  <li><a href="#0105-pdt---raw-whole-file-cd-read-for-the-first-activitypak-header-still-fails-the-same-way" id="markdown-toc-0105-pdt---raw-whole-file-cd-read-for-the-first-activitypak-header-still-fails-the-same-way">01:05 PDT - Raw whole-file CD read for the first <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header still fails the same way</a></li>
  <li><a href="#0110-pdt---cdsearchfile-metadata-for-activitypak-is-already-wrong-before-any-header-bytes-are-read" id="markdown-toc-0110-pdt---cdsearchfile-metadata-for-activitypak-is-already-wrong-before-any-header-bytes-are-read">01:10 PDT - <code class="language-plaintext highlighter-rouge">CdSearchFile</code> metadata for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> is already wrong before any header bytes are read</a></li>
  <li><a href="#0116-pdt---cdsearchfile-is-not-just-returning-the-wrong-size-the-resolved-activitypak-start-sector-is-wrong-too" id="markdown-toc-0116-pdt---cdsearchfile-is-not-just-returning-the-wrong-size-the-resolved-activitypak-start-sector-is-wrong-too">01:16 PDT - <code class="language-plaintext highlighter-rouge">CdSearchFile</code> is not just returning the wrong size; the resolved <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> start sector is wrong too</a></li>
  <li><a href="#0120-pdt---resetting-cd-state-immediately-before-activitypak-lookup-does-not-recover-the-scene" id="markdown-toc-0120-pdt---resetting-cd-state-immediately-before-activitypak-lookup-does-not-recover-the-scene">01:20 PDT - Resetting CD state immediately before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup does not recover the scene</a></li>
  <li><a href="#2258-pdt---activity-4-on-6dae8410-story-single-was-broken-on-ps1-now-narrowed-to-exact-scene-no-launch" id="markdown-toc-2258-pdt---activity-4-on-6dae8410-story-single-was-broken-on-ps1-now-narrowed-to-exact-scene-no-launch">22:58 PDT - ACTIVITY 4 on <code class="language-plaintext highlighter-rouge">6dae8410</code>: <code class="language-plaintext highlighter-rouge">story single</code> was broken on PS1; now narrowed to exact-scene no-launch</a></li>
  <li><a href="#2307-pdt---direct-ads-activityads-4-matches-story-single-4-byte-for-byte-on-key-frames" id="markdown-toc-2307-pdt---direct-ads-activityads-4-matches-story-single-4-byte-for-byte-on-key-frames">23:07 PDT - Direct <code class="language-plaintext highlighter-rouge">ads ACTIVITY.ADS 4</code> matches <code class="language-plaintext highlighter-rouge">story single 4</code> byte-for-byte on key frames</a></li>
  <li><a href="#2314-pdt---activityads-4-is-not-the-same-first-chunk-zero-thread-failure-shape-as-activity-1" id="markdown-toc-2314-pdt---activityads-4-is-not-the-same-first-chunk-zero-thread-failure-shape-as-activity-1">23:14 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> is not the same first-chunk zero-thread failure shape as <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></a></li>
  <li><a href="#2320-pdt---activityads-4-startup-chain-proof-is-also-a-clean-negative" id="markdown-toc-2320-pdt---activityads-4-startup-chain-proof-is-also-a-clean-negative">23:20 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> startup-chain proof is also a clean negative</a></li>
  <li><a href="#2331-pdt---post-ttmloadttm-slot-2-visual-proofs-are-also-negative-for-activityads-4" id="markdown-toc-2331-pdt---post-ttmloadttm-slot-2-visual-proofs-are-also-negative-for-activityads-4">23:31 PDT - Post-<code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> slot-2 visual proofs are also negative for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code></a></li>
  <li><a href="#2339-pdt---even-a-top-of-adsplayactivityads-4-visible-return-is-inert-on-the-captured-output" id="markdown-toc-2339-pdt---even-a-top-of-adsplayactivityads-4-visible-return-is-inert-on-the-captured-output">23:39 PDT - Even a top-of-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 4)</code> visible return is inert on the captured output</a></li>
  <li><a href="#2345-pdt---non-visual-validation-confirmed-exact-adsplayactivityads-4-proof-does-execute-even-though-frames-do-not-move" id="markdown-toc-2345-pdt---non-visual-validation-confirmed-exact-adsplayactivityads-4-proof-does-execute-even-though-frames-do-not-move">23:45 PDT - Non-visual validation confirmed: exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 4)</code> proof does execute even though frames do not move</a></li>
  <li><a href="#2346-pdt---fresh-build-timeout-proof-shows-island-ads-activityads-4-is-not-reaching-mains-argads-branch-on-6dae8410" id="markdown-toc-2346-pdt---fresh-build-timeout-proof-shows-island-ads-activityads-4-is-not-reaching-mains-argads-branch-on-6dae8410">23:46 PDT - Fresh-build timeout proof shows <code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code> is not reaching <code class="language-plaintext highlighter-rouge">main()</code>’s <code class="language-plaintext highlighter-rouge">argAds</code> branch on <code class="language-plaintext highlighter-rouge">6dae8410</code></a></li>
  <li><a href="#2154-pdt---adsload-heap-allocation-was-corrupting-activity-bootstrap-static-ads-tag-storage-removes-the-old-ocean-failure" id="markdown-toc-2154-pdt---adsload-heap-allocation-was-corrupting-activity-bootstrap-static-ads-tag-storage-removes-the-old-ocean-failure">21:54 PDT - <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> heap allocation was corrupting ACTIVITY bootstrap; static ADS tag storage removes the old ocean failure</a></li>
  <li><a href="#2236-pdt---step-back-validation-6dae8410-is-a-materially-better-ps1-base-than-current-head-for-activity-4" id="markdown-toc-2236-pdt---step-back-validation-6dae8410-is-a-materially-better-ps1-base-than-current-head-for-activity-4">22:36 PDT - Step-back validation: <code class="language-plaintext highlighter-rouge">6dae8410</code> is a materially better PS1 base than current HEAD for <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code></a></li>
  <li><a href="#2120-pdt---activity-startup-failure-is-now-narrowed-to-missing-slot-1-resource-binding-before-first-chunk-playback" id="markdown-toc-2120-pdt---activity-startup-failure-is-now-narrowed-to-missing-slot-1-resource-binding-before-first-chunk-playback">21:20 PDT - ACTIVITY startup failure is now narrowed to missing slot-1 resource binding before first chunk playback</a></li>
  <li><a href="#2135-pdt---raw-resource001-parse-confirms-activity-source-asset-is-correct" id="markdown-toc-2135-pdt---raw-resource001-parse-confirms-activity-source-asset-is-correct">21:35 PDT - Raw <code class="language-plaintext highlighter-rouge">RESOURCE.001</code> parse confirms ACTIVITY source asset is correct</a></li>
  <li><a href="#2145-pdt---first-exact-activity-in-memory-ads-list-and-slot-1-ttm-load-are-both-correct" id="markdown-toc-2145-pdt---first-exact-activity-in-memory-ads-list-and-slot-1-ttm-load-are-both-correct">21:45 PDT - First exact ACTIVITY in-memory ADS list and slot-1 TTM load are both correct</a></li>
  <li><a href="#2155-pdt---adsload-is-the-active-boundary-but-not-via-simple-tagchunk-table-overflow" id="markdown-toc-2155-pdt---adsload-is-the-active-boundary-but-not-via-simple-tagchunk-table-overflow">21:55 PDT - <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> is the active boundary, but not via simple tag/chunk table overflow</a></li>
  <li><a href="#2016-pdt---quadrant-color-upload-proof-collapsed-the-late-activity-frames-to-near-black-but-did-not-identify-tile-ownership" id="markdown-toc-2016-pdt---quadrant-color-upload-proof-collapsed-the-late-activity-frames-to-near-black-but-did-not-identify-tile-ownership">20:16 PDT - Quadrant-color upload proof collapsed the late ACTIVITY frames to near-black, but did not identify tile ownership</a></li>
  <li><a href="#2028-pdt---by-0490005000-no-normal-background-tile-uploads-are-active-anymore-on-the-stable-activity-failure-path" id="markdown-toc-2028-pdt---by-0490005000-no-normal-background-tile-uploads-are-active-anymore-on-the-stable-activity-failure-path">20:28 PDT - By <code class="language-plaintext highlighter-rouge">04900..05000</code>, no normal background-tile uploads are active anymore on the stable ACTIVITY failure path</a></li>
  <li><a href="#2033-pdt---the-last-decoded-normal-background-upload-in-the-activity-run-is-back-at-frame_0610" id="markdown-toc-2033-pdt---the-last-decoded-normal-background-upload-in-the-activity-run-is-back-at-frame_0610">20:33 PDT - The last decoded normal background upload in the ACTIVITY run is back at <code class="language-plaintext highlighter-rouge">frame_0610</code></a></li>
  <li><a href="#2043-pdt---one-shot-post-init-framebuffer-clear-removes-the-stale-04900-title-but-not-the-settled-04910-ocean" id="markdown-toc-2043-pdt---one-shot-post-init-framebuffer-clear-removes-the-stale-04900-title-but-not-the-settled-04910-ocean">20:43 PDT - One-shot post-init framebuffer clear removes the stale <code class="language-plaintext highlighter-rouge">04900</code> title, but not the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</a></li>
  <li><a href="#2046-pdt---the-settled-04910-activity-ocean-comes-directly-from-the-exact-ocean0scr-bootstrap-load-in-islandinit" id="markdown-toc-2046-pdt---the-settled-04910-activity-ocean-comes-directly-from-the-exact-ocean0scr-bootstrap-load-in-islandinit">20:46 PDT - The settled <code class="language-plaintext highlighter-rouge">04910+</code> ACTIVITY ocean comes directly from the exact <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> bootstrap load in <code class="language-plaintext highlighter-rouge">islandInit()</code></a></li>
  <li><a href="#2052-pdt---direct-black-and-return-at-the-top-of-storyplaypreparedsceneactivityads-1-still-does-not-move-the-settled-04910-ocean" id="markdown-toc-2052-pdt---direct-black-and-return-at-the-top-of-storyplaypreparedsceneactivityads-1-still-does-not-move-the-settled-04910-ocean">20:52 PDT - Direct black-and-return at the top of <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> still does not move the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</a></li>
  <li><a href="#2056-pdt---even-returning-from-storyplay-immediately-after-an-exact-no-launch-still-does-not-move-the-settled-04910-ocean" id="markdown-toc-2056-pdt---even-returning-from-storyplay-immediately-after-an-exact-no-launch-still-does-not-move-the-settled-04910-ocean">20:56 PDT - Even returning from <code class="language-plaintext highlighter-rouge">storyPlay()</code> immediately after an exact no-launch still does not move the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</a></li>
  <li><a href="#2101-pdt---if-exact-activity-still-has-numthreads--0-immediately-after-the-first-adsplaychunk-the-whole-late-boundary-turns-black" id="markdown-toc-2101-pdt---if-exact-activity-still-has-numthreads--0-immediately-after-the-first-adsplaychunk-the-whole-late-boundary-turns-black">21:01 PDT - If exact ACTIVITY still has <code class="language-plaintext highlighter-rouge">numThreads == 0</code> immediately after the first <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code>, the whole late boundary turns black</a></li>
  <li><a href="#2106-pdt---the-first-exact-activity-chunk-definitely-reaches-authored-startup-add_scene112--add_scene113" id="markdown-toc-2106-pdt---the-first-exact-activity-chunk-definitely-reaches-authored-startup-add_scene112--add_scene113">21:06 PDT - The first exact ACTIVITY chunk definitely reaches authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,12)</code> / <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,13)</code></a></li>
  <li><a href="#1939-pdt---late-activity-background-thread-suppression-does-not-move-the-settled-ocean" id="markdown-toc-1939-pdt---late-activity-background-thread-suppression-does-not-move-the-settled-ocean">19:39 PDT - Late ACTIVITY background-thread suppression does not move the settled ocean</a></li>
  <li><a href="#1941-pdt---late-ttmplay-suppression-also-does-not-move-the-settled-ocean" id="markdown-toc-1941-pdt---late-ttmplay-suppression-also-does-not-move-the-settled-ocean">19:41 PDT - Late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> suppression also does not move the settled ocean</a></li>
  <li><a href="#1943-pdt---zeroing-all-background-tiles-immediately-before-loadimage-still-does-not-move-the-settled-ocean" id="markdown-toc-1943-pdt---zeroing-all-background-tiles-immediately-before-loadimage-still-does-not-move-the-settled-ocean">19:43 PDT - Zeroing all background tiles immediately before <code class="language-plaintext highlighter-rouge">LoadImage()</code> still does not move the settled ocean</a></li>
  <li><a href="#1947-pdt---direct-late-framebuffer-black-upload-does-change-the-settled-activity-image" id="markdown-toc-1947-pdt---direct-late-framebuffer-black-upload-does-change-the-settled-activity-image">19:47 PDT - Direct late framebuffer black upload does change the settled ACTIVITY image</a></li>
  <li><a href="#1951-pdt---replacing-the-grdrawbackground-upload-source-with-black-does-change-the-settled-activity-image" id="markdown-toc-1951-pdt---replacing-the-grdrawbackground-upload-source-with-black-does-change-the-settled-activity-image">19:51 PDT - Replacing the <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload source with black does change the settled ACTIVITY image</a></li>
  <li><a href="#1956-pdt---overwriting-the-exact-heap-upload-slice-still-does-not-move-the-settled-ocean" id="markdown-toc-1956-pdt---overwriting-the-exact-heap-upload-slice-still-does-not-move-the-settled-ocean">19:56 PDT - Overwriting the exact heap upload slice still does not move the settled ocean</a></li>
  <li><a href="#2000-pdt---fresh-address-upload-only-perturbs-the-lead-in-not-the-settled-04910-ocean" id="markdown-toc-2000-pdt---fresh-address-upload-only-perturbs-the-lead-in-not-the-settled-04910-ocean">20:00 PDT - Fresh-address upload only perturbs the lead-in, not the settled <code class="language-plaintext highlighter-rouge">04910</code> ocean</a></li>
  <li><a href="#2001-pdt---widened-fresh-address-upload-window-is-cleanly-negative" id="markdown-toc-2001-pdt---widened-fresh-address-upload-window-is-cleanly-negative">20:01 PDT - Widened fresh-address upload window is cleanly negative</a></li>
  <li><a href="#2005-pdt---forcing-full-height-uploads-across-0490004910-makes-the-settled-ocean-disappear" id="markdown-toc-2005-pdt---forcing-full-height-uploads-across-0490004910-makes-the-settled-ocean-disappear">20:05 PDT - Forcing full-height uploads across <code class="language-plaintext highlighter-rouge">04900–04910</code> makes the settled ocean disappear</a></li>
  <li><a href="#2010-pdt---forcing-full-height-uploads-with-the-normal-source-does-nothing" id="markdown-toc-2010-pdt---forcing-full-height-uploads-with-the-normal-source-does-nothing">20:10 PDT - Forcing full-height uploads with the normal source does nothing</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing" id="markdown-toc-1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</a></li>
  <li><a href="#1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched" id="markdown-toc-1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</a></li>
  <li><a href="#1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing" id="markdown-toc-1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</a></li>
  <li><a href="#1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash" id="markdown-toc-1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash">19:26 PDT - Zeroing restored background tiles every frame still leaves the exact original ocean hash</a></li>
  <li><a href="#1930-pdt---disabling-saved-rect-replay-still-leaves-the-exact-original-ocean-hash" id="markdown-toc-1930-pdt---disabling-saved-rect-replay-still-leaves-the-exact-original-ocean-hash">19:30 PDT - Disabling saved-rect replay still leaves the exact original ocean hash</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-1" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-1">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-1" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-1">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-1" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-1">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-1" id="markdown-toc-1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-1">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</a></li>
  <li><a href="#1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-1" id="markdown-toc-1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-1">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</a></li>
  <li><a href="#1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-1" id="markdown-toc-1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-1">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</a></li>
  <li><a href="#1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash-1" id="markdown-toc-1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash-1">19:26 PDT - Zeroing restored background tiles every frame still leaves the exact original ocean hash</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-2" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-2">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-2" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-2">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-2" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-2">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-2" id="markdown-toc-1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-2">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</a></li>
  <li><a href="#1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-2" id="markdown-toc-1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-2">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</a></li>
  <li><a href="#1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-2" id="markdown-toc-1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-2">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-3" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-3">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-3" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-3">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-3" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-3">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-3" id="markdown-toc-1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-3">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</a></li>
  <li><a href="#1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-3" id="markdown-toc-1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-3">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-4" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-4">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-4" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-4">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-4" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-4">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-4" id="markdown-toc-1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-4">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-5" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-5">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-5" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-5">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-5" id="markdown-toc-1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-5">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-6" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-6">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-6" id="markdown-toc-1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-6">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</a></li>
  <li><a href="#1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-7" id="markdown-toc-1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-7">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</a></li>
  <li><a href="#1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910-1" id="markdown-toc-1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910-1">18:45 PDT - Skipping exact scene island-state calculation perturbs the lead-in, but ocean still settles by <code class="language-plaintext highlighter-rouge">04910</code></a></li>
</ul>

</details>

<h2 id="2026-03-28-1259-pdt">2026-03-28 12:59 PDT</h2>

<ul>
  <li>Found and fixed a real PS1 libc corruption source in <code class="language-plaintext highlighter-rouge">ps1_stubs.c</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">vprintf()</code> was using raw <code class="language-plaintext highlighter-rouge">vsprintf()</code> into a fixed <code class="language-plaintext highlighter-rouge">char buffer[1024]</code></li>
      <li>switched it to bounded <code class="language-plaintext highlighter-rouge">vsnprintf(buffer, sizeof(buffer), ...)</code></li>
    </ul>
  </li>
  <li>Validation result:
    <ul>
      <li>this fix was real but not sufficient by itself</li>
      <li>the focused <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> rerun still eventually degraded into <code class="language-plaintext highlighter-rouge">UnknownReadHandler</code> / <code class="language-plaintext highlighter-rouge">UnknownWriteHandler</code> at <code class="language-plaintext highlighter-rouge">pc 0x8002D88C / 0x8002C998</code></li>
      <li>however, the corruption wave moved much later in wall-clock time, which proved the <code class="language-plaintext highlighter-rouge">vprintf</code> overflow was one contributor</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1430-pdt">2026-03-28 14:30 PDT</h2>

<ul>
  <li>Added later-runtime boot-state probes instead of early boot <code class="language-plaintext highlighter-rouge">printf</code>/<code class="language-plaintext highlighter-rouge">fatalError</code> traps.</li>
  <li>The compact boot bit-cell rows and the enlarged story bar panel are both too visually contaminated in live frames to trust numerically:
    <ul>
      <li>at <code class="language-plaintext highlighter-rouge">frame_02700</code> in the stable wrong-ocean window, the new boot-state rows still decode as zero</li>
      <li>but the story bars saturate to junk widths like <code class="language-plaintext highlighter-rouge">90</code>, so those panels are not a reliable proof surface</li>
    </ul>
  </li>
  <li>Switched to clean late yes/no traps in <code class="language-plaintext highlighter-rouge">story.c</code>:
    <ol>
      <li>trap if <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> is entered and launch fails</li>
      <li>trap if <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> is entered at all</li>
      <li>trap if <code class="language-plaintext highlighter-rouge">storyPlay()</code> reaches the <code class="language-plaintext highlighter-rouge">storyBootAdsName/storyBootAdsTag</code> boot-scene branch for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
    </ol>
  </li>
  <li>Results:
    <ul>
      <li>none of those traps fired in stable exact PS1 runs</li>
      <li>not by <code class="language-plaintext highlighter-rouge">2700</code> frames:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-launchfailtrap/result.json</code></li>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootbranchtrap/result.json</code></li>
        </ul>
      </li>
      <li>and not even by <code class="language-plaintext highlighter-rouge">6300</code> frames for the prepared-scene entry trap:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-entrytrap-6300/result.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>This is the cleanest narrowing so far:
    <ul>
      <li>the stable PS1 path is not reaching <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code></li>
      <li>and it is not even reaching the <code class="language-plaintext highlighter-rouge">storyPlay()</code> boot-scene resolution branch for <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code></li>
      <li>so the active root cause is upstream of ADS launch and upstream of story scene dispatch</li>
    </ul>
  </li>
  <li>Current best hypothesis:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code> is present on disc, but the <code class="language-plaintext highlighter-rouge">story ads ...</code> override is not being retained/applied by the runtime path that reaches <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>the next target is the PS1 boot-override load/reapply path, not ACTIVITY ADS internals</li>
    </ul>
  </li>
  <li>Follow-up probes on the same branch:
    <ul>
      <li>Added a trap in <code class="language-plaintext highlighter-rouge">storyPlay()</code> itself to dump raw boot-state (<code class="language-plaintext highlighter-rouge">storyBootSingleSceneIndex</code>, <code class="language-plaintext highlighter-rouge">storyBootSceneIndex</code>, <code class="language-plaintext highlighter-rouge">storyBootAdsName</code>, <code class="language-plaintext highlighter-rouge">storyBootAdsTag</code>) on loop entry.</li>
      <li>The run terminated extremely early with only 3 captured frames:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-storyloopbootstate/result.json</code></li>
        </ul>
      </li>
      <li>But, as with earlier very-early guest traps, the expected guest fatal text still did not surface in the extracted logs.</li>
    </ul>
  </li>
  <li>Practical interpretation:
    <ul>
      <li>early/pre-dispatch guest fatal output remains an unreliable proof surface under the current regtest path</li>
      <li>later negative traps remain trustworthy:
        <ul>
          <li>the stable path still does not reach <code class="language-plaintext highlighter-rouge">storyPlay()</code>’s boot-scene branch for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
          <li>and still does not reach <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> by <code class="language-plaintext highlighter-rouge">6300</code></li>
        </ul>
      </li>
      <li>so the active bug remains upstream of story scene dispatch, in PS1 boot-override retention/application or in the transition from boot parsing into the story loop</li>
    </ul>
  </li>
  <li>Then removed the remaining live PS1 scene-trace <code class="language-plaintext highlighter-rouge">printf()</code> traffic from the hot <code class="language-plaintext highlighter-rouge">ACTIVITY</code> path while preserving the strip markers and the hard zero-IP trap:
    <ul>
      <li>removed <code class="language-plaintext highlighter-rouge">[STORY] ...</code> boot/final prints from <code class="language-plaintext highlighter-rouge">repo:/story.c</code></li>
      <li>removed <code class="language-plaintext highlighter-rouge">[ADS] initial launch empty ...</code>, <code class="language-plaintext highlighter-rouge">[ADSPLAY] after chunk ...</code>, and <code class="language-plaintext highlighter-rouge">[ACT1LIVE] ...</code> prints from <code class="language-plaintext highlighter-rouge">repo:/ads.c</code></li>
    </ul>
  </li>
  <li>This produced the first clean long <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> PS1 run without fatal corruption:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">frames = 6300</code></li>
      <li><code class="language-plaintext highlighter-rouge">interval = 150</code></li>
      <li><code class="language-plaintext highlighter-rouge">exit_code = 0</code></li>
      <li><code class="language-plaintext highlighter-rouge">timed_out = false</code></li>
      <li><code class="language-plaintext highlighter-rouge">frames_captured = 41</code></li>
      <li><code class="language-plaintext highlighter-rouge">has_fatal_error = false</code></li>
    </ul>
  </li>
  <li>Clean visual timeline from that run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_02700</code> through <code class="language-plaintext highlighter-rouge">frame_06000</code>: still wrong <code class="language-plaintext highlighter-rouge">ocean</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code> and <code class="language-plaintext highlighter-rouge">frame_06300</code>: wrong live <code class="language-plaintext highlighter-rouge">island</code> / <code class="language-plaintext highlighter-rouge">FISHING</code>-like scene</li>
    </ul>
  </li>
  <li>Clean robust-strip decode at the handoff:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/20260328-125856/frames/jcreborn/frame_06000.png</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">scene_markers launched=no add_scene=no tag_hit=yes tag_miss=yes bmp_ok=no sprite_seen=yes</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/20260328-125856/frames/jcreborn/frame_06150.png</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">scene_markers launched=yes add_scene=yes tag_hit=no tag_miss=no bmp_ok=yes sprite_seen=no</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/20260328-125856/frames/jcreborn/frame_06300.png</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">scene_markers launched=yes add_scene=yes tag_hit=no tag_miss=no bmp_ok=yes sprite_seen=no</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current read after removing the corrupting trace path:
    <ul>
      <li>the earlier catastrophic memory corruption was at least partly self-induced by our PS1 <code class="language-plaintext highlighter-rouge">printf</code> tracing</li>
      <li>the underlying <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> scene bug still remains, now exposed cleanly again</li>
      <li>the real failure shape is:
        <ul>
          <li>failed-launch / wrong-ocean window through <code class="language-plaintext highlighter-rouge">6000</code></li>
          <li>then wrong live <code class="language-plaintext highlighter-rouge">FISHING</code>-like scene by <code class="language-plaintext highlighter-rouge">6150</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>compare this clean <code class="language-plaintext highlighter-rouge">quiettrace6300</code> PS1 run directly against the canonical host <code class="language-plaintext highlighter-rouge">ACTIVITY-1</code> reference window</li>
      <li>then resume debugging the actual <code class="language-plaintext highlighter-rouge">6000-6150</code> handoff with strip markers and narrow traps only, not live formatted PS1 logging</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1302-pdt">2026-03-28 13:02 PDT</h2>

<ul>
  <li>Completed the clean PS1-vs-host validation on the no-printf <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> run:
    <ul>
      <li>PS1 run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/result.json</code></li>
        </ul>
      </li>
      <li>compare result:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/activity1-quiettrace6300-vs-host.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Compare verdict:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">verdict = PIXEL_MISMATCH</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_offset = 1800</code></li>
      <li><code class="language-plaintext highlighter-rouge">common_frame_count = 3</code></li>
      <li><code class="language-plaintext highlighter-rouge">reference_coverage_ratio = 0.008426966292134831</code></li>
      <li><code class="language-plaintext highlighter-rouge">average_palette_index_diff_pixels = 126892.66666666667</code></li>
    </ul>
  </li>
  <li>Practical meaning:
    <ul>
      <li>the harness is now doing its job on a clean PS1 run</li>
      <li>the PS1 <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> sequence only brushes the host scene window at entry, then diverges almost immediately</li>
      <li>this is a real scene/content bug, not a remaining harness artifact</li>
    </ul>
  </li>
  <li>Clean visual shape remains:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_02700</code> through <code class="language-plaintext highlighter-rouge">frame_06000</code>: wrong <code class="language-plaintext highlighter-rouge">ocean</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code> and <code class="language-plaintext highlighter-rouge">frame_06300</code>: wrong <code class="language-plaintext highlighter-rouge">island</code> / <code class="language-plaintext highlighter-rouge">FISHING</code>-like scene</li>
    </ul>
  </li>
  <li>Current confidence statement:
    <ul>
      <li>host baseline path is trusted</li>
      <li>sequence comparator is trusted for this scene</li>
      <li>PS1 logging-induced corruption has been reduced enough that the real <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> failure is visible again</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>keep <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> as the first active PS1 fix</li>
      <li>debug the actual <code class="language-plaintext highlighter-rouge">6000-6150</code> handoff on the clean quiet path, without restoring the old formatted PS1 trace traffic</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1331-pdt">2026-03-28 13:31 PDT</h2>

<ul>
  <li>Ran a widened <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> live-tag probe on the clean quiet path:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-livetags/result.json</code></li>
    </ul>
  </li>
  <li>The run completed cleanly through <code class="language-plaintext highlighter-rouge">6300</code> frames and kept the same visible failure shape:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: wrong <code class="language-plaintext highlighter-rouge">ocean</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code>, <code class="language-plaintext highlighter-rouge">frame_06300</code>: wrong live <code class="language-plaintext highlighter-rouge">island</code> scene</li>
    </ul>
  </li>
  <li>Decoded robust-strip state at <code class="language-plaintext highlighter-rouge">frame_06150</code> / <code class="language-plaintext highlighter-rouge">frame_06300</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">current_ads_family_estimate = 1</code></li>
      <li><code class="language-plaintext highlighter-rouge">current_ads_tag_estimate = 1</code></li>
      <li><code class="language-plaintext highlighter-rouge">launched = true</code></li>
      <li><code class="language-plaintext highlighter-rouge">add_scene = true</code></li>
      <li><code class="language-plaintext highlighter-rouge">zero_ip_launch = true</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_live_slot2 = true</code></li>
      <li><code class="language-plaintext highlighter-rouge">activity_live_ip_zero = true</code></li>
      <li>authored ACTIVITY live-tag one-hots remain all false for the currently instrumented tags:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag7 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag8 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag9 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag12 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag14 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag2 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag11 = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">activity_live_tag13 = false</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Practical read:
    <ul>
      <li>the wrong live scene still appears to be occurring inside <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS tag 1</code>, not from a clean family switch</li>
      <li>but the active live thread visible in the robust strip still does not match any authored ACTIVITY tag we were explicitly drawing</li>
    </ul>
  </li>
  <li>Added a more direct overlay probe:
    <ul>
      <li>raw robust rows for exact <code class="language-plaintext highlighter-rouge">firstIdx.sceneSlot</code> low bits</li>
      <li>raw robust rows for exact <code class="language-plaintext highlighter-rouge">firstIdx.sceneTag</code> low bits</li>
      <li>decoder support for <code class="language-plaintext highlighter-rouge">live_scene_slot_robust</code> / <code class="language-plaintext highlighter-rouge">live_scene_tag_robust</code></li>
    </ul>
  </li>
  <li>That follow-up rerun did <strong>not</strong> stay clean:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-livetags-robust/regtest.log</code></li>
      <li>it regressed into heavy <code class="language-plaintext highlighter-rouge">UnknownReadHandler</code> spam at <code class="language-plaintext highlighter-rouge">pc 0x8002D9FC</code> before producing trustworthy postprocess output</li>
    </ul>
  </li>
  <li>Current state:
    <ul>
      <li>harness remains good enough on the clean <code class="language-plaintext highlighter-rouge">quiettrace6300</code> / <code class="language-plaintext highlighter-rouge">livetags</code> path</li>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> is still the first real scene bug</li>
      <li>the next fix/debug target is still the ACTIVITY handoff itself, but the new exact-tag probe needs to be rerun in a way that preserves the clean runtime behavior</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1340-pdt">2026-03-28 13:40 PDT</h2>

<ul>
  <li>Reverted the invasive exact-tag overlay rows after they destabilized the runtime, and switched to one-shot engine traps instead.</li>
  <li>Two focused ACTIVITY probes completed cleanly:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-zeroip-launchtrap/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-startup-alive-trap/result.json</code></li>
    </ul>
  </li>
  <li>New stable behavior on both runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06000</code>, <code class="language-plaintext highlighter-rouge">frame_06150</code>, and <code class="language-plaintext highlighter-rouge">frame_06300</code> all remain wrong <code class="language-plaintext highlighter-rouge">ocean</code></li>
      <li>the earlier wrong-island handoff is not stable enough to treat as the real bug surface</li>
    </ul>
  </li>
  <li>Important negative results:
    <ul>
      <li>the non-background zero-<code class="language-plaintext highlighter-rouge">ip</code> launch trap did <strong>not</strong> fire after frame <code class="language-plaintext highlighter-rouge">5000</code></li>
      <li>the startup-thread-alive trap for <code class="language-plaintext highlighter-rouge">ACTIVITY slot1 tag12/tag13</code> did <strong>not</strong> fire after frame <code class="language-plaintext highlighter-rouge">5000</code></li>
    </ul>
  </li>
  <li>Static authored check:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">GJDIVE.TTM tag 12</code> reaches <code class="language-plaintext highlighter-rouge">0x0110</code> and then <code class="language-plaintext highlighter-rouge">0x0FF0</code> almost immediately, so it should not live for thousands of frames</li>
      <li>the fact that the engine trap did not see tag <code class="language-plaintext highlighter-rouge">12</code> or <code class="language-plaintext highlighter-rouge">13</code> still running means the older strip decode on those startup bits was over-reporting</li>
    </ul>
  </li>
  <li>Current stable read:
    <ul>
      <li>harness is still at practical confidence</li>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> does not reach the intended ACTIVITY scene content on PS1</li>
      <li>the stable bug surface is now: booted ACTIVITY scene collapses back to ocean and stays there through <code class="language-plaintext highlighter-rouge">6300</code></li>
      <li>the immediate next target is no longer “later ACTIVITY handoff tag”; it is “why no later successful ACTIVITY launch happens at all on the stable run”</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1346-pdt">2026-03-28 13:46 PDT</h2>

<ul>
  <li>Pulled the authored startup graph directly from generated analysis and extracted bytecode:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS tag 1</code> starts with <code class="language-plaintext highlighter-rouge">slot 1 tag 12</code> and <code class="language-plaintext highlighter-rouge">slot 1 tag 13</code></li>
      <li>later authored graph contains <code class="language-plaintext highlighter-rouge">slot 1 tags 7/8/9/14/11</code> and <code class="language-plaintext highlighter-rouge">slot 2 tag 2</code></li>
    </ul>
  </li>
  <li>Dumped <code class="language-plaintext highlighter-rouge">GJDIVE.TTM tag 12</code> from extracted content:
    <ul>
      <li>it reaches <code class="language-plaintext highlighter-rouge">0x0110</code> and then <code class="language-plaintext highlighter-rouge">0x0FF0</code> almost immediately</li>
      <li>so it should terminate quickly, not remain the active long-lived state</li>
    </ul>
  </li>
  <li>Added and ran a one-shot trigger trap in <code class="language-plaintext highlighter-rouge">adsPlayTriggeredChunks()</code> for <code class="language-plaintext highlighter-rouge">ACTIVITY slot1 tag12/tag13</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-trigger-trap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>trap did <strong>not</strong> fire</li>
      <li>stable visual output remained wrong <code class="language-plaintext highlighter-rouge">ocean</code> through <code class="language-plaintext highlighter-rouge">6300</code></li>
      <li>no evidence that startup <code class="language-plaintext highlighter-rouge">tag 12/13</code> transitions are reaching the authored trigger path on the stable PS1 run</li>
    </ul>
  </li>
  <li>Current read:
    <ul>
      <li>the stable ACTIVITY bug is now below later scene handoff logic</li>
      <li>the next likely fault surface is the startup-thread termination / trigger pipeline itself:
        <ul>
          <li>either the startup thread is not terminating in the way the ADS scheduler expects</li>
          <li>or the termination is happening without the expected <code class="language-plaintext highlighter-rouge">adsPlayTriggeredChunks(slot=1, tag=12/13)</code> handoff</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1351-pdt">2026-03-28 13:51 PDT</h2>

<ul>
  <li>Added and ran a one-shot termination trap on the ACTIVITY startup threads:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-terminate-trap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>trap did <strong>not</strong> fire</li>
      <li>run stayed wrong <code class="language-plaintext highlighter-rouge">ocean</code> through <code class="language-plaintext highlighter-rouge">6300</code></li>
      <li>visual/state hash remained on the same stable bad branch as the trigger-trap run</li>
    </ul>
  </li>
  <li>Combined with the previous negative trigger result:
    <ul>
      <li>startup <code class="language-plaintext highlighter-rouge">tag 12/13</code> is not reaching <code class="language-plaintext highlighter-rouge">adsPlayTriggeredChunks(slot=1, tag=12/13)</code></li>
      <li>and it is also not entering the normal <code class="language-plaintext highlighter-rouge">ADS_THREAD_TERMINATED</code> branch in a way the engine sees</li>
    </ul>
  </li>
  <li>Current narrowest read:
    <ul>
      <li>the ACTIVITY startup branch is being lost, overwritten, or bypassed before normal termination/trigger processing</li>
      <li>next target should move earlier than <code class="language-plaintext highlighter-rouge">ADS_THREAD_TERMINATED</code>, likely around:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ttmPlay()</code> changing <code class="language-plaintext highlighter-rouge">isRunning</code></li>
          <li><code class="language-plaintext highlighter-rouge">nextGotoOffset</code> / <code class="language-plaintext highlighter-rouge">ip</code> evolution for <code class="language-plaintext highlighter-rouge">GJDIVE tag 12/13</code></li>
          <li>or direct thread-slot reuse before the expected termination path</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1355-pdt">2026-03-28 13:55 PDT</h2>

<ul>
  <li>Added a one-shot opcode trap directly inside <code class="language-plaintext highlighter-rouge">ttmPlay()</code> for ACTIVITY startup tags <code class="language-plaintext highlighter-rouge">12/13</code> at:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">0x0110</code> (<code class="language-plaintext highlighter-rouge">PURGE</code>)</li>
      <li><code class="language-plaintext highlighter-rouge">0x0FF0</code> (<code class="language-plaintext highlighter-rouge">UPDATE</code>)</li>
    </ul>
  </li>
  <li>Reran the exact ACTIVITY boot:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-ttm-opcode-trap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>trap did <strong>not</strong> fire</li>
      <li>run again stayed on the same stable bad branch: wrong <code class="language-plaintext highlighter-rouge">ocean</code> through <code class="language-plaintext highlighter-rouge">6300</code></li>
    </ul>
  </li>
  <li>Combined with earlier negatives:
    <ul>
      <li>startup <code class="language-plaintext highlighter-rouge">tag 12/13</code> does not reach normal trigger handoff</li>
      <li>does not enter the observed <code class="language-plaintext highlighter-rouge">ADS_THREAD_TERMINATED</code> branch</li>
      <li>and now does not hit the expected authored early <code class="language-plaintext highlighter-rouge">PURGE</code> / <code class="language-plaintext highlighter-rouge">UPDATE</code> path in a way the runtime exposes</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the ACTIVITY startup thread is diverging before normal TTM control flow</li>
      <li>next target must move even earlier than opcode-level lifetime handling:
        <ul>
          <li>startup thread <code class="language-plaintext highlighter-rouge">ip</code> initialization</li>
          <li>TTM slot/data binding</li>
          <li>or thread-slot reuse/corruption before the first expected <code class="language-plaintext highlighter-rouge">UPDATE</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1359-pdt">2026-03-28 13:59 PDT</h2>

<ul>
  <li>Added the earliest startup-birth trap so <code class="language-plaintext highlighter-rouge">ACTIVITY slot1 tag12/tag13</code> would abort immediately after <code class="language-plaintext highlighter-rouge">adsAddScene()</code> assigns <code class="language-plaintext highlighter-rouge">ip</code>:
    <ul>
      <li>trap condition:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1AdsCurrentName == ACTIVITY.ADS</code></li>
          <li><code class="language-plaintext highlighter-rouge">ps1AdsCurrentTag == 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">ttmSlotNo == 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">ttmTag == 12 || ttmTag == 13</code></li>
        </ul>
      </li>
      <li>emitted fields if hit:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame</code></li>
          <li><code class="language-plaintext highlighter-rouge">idx</code></li>
          <li><code class="language-plaintext highlighter-rouge">slot</code></li>
          <li><code class="language-plaintext highlighter-rouge">tag</code></li>
          <li><code class="language-plaintext highlighter-rouge">ip</code></li>
          <li><code class="language-plaintext highlighter-rouge">dataSize</code></li>
          <li><code class="language-plaintext highlighter-rouge">firstOpcode</code></li>
          <li><code class="language-plaintext highlighter-rouge">arg3</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Reran the exact scene on that build:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-addscene-birth-trap</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the birth trap did <strong>not</strong> fire</li>
      <li>no <code class="language-plaintext highlighter-rouge">ACT1 addScene ...</code> fatal line appeared in <code class="language-plaintext highlighter-rouge">printf.log</code>, <code class="language-plaintext highlighter-rouge">regtest.log</code>, or extracted <code class="language-plaintext highlighter-rouge">tty-output.txt</code></li>
      <li>the run still completed capture/postprocess on the same stable bad branch</li>
    </ul>
  </li>
  <li>This is stronger than the later negative traps:
    <ul>
      <li>startup <code class="language-plaintext highlighter-rouge">tag12/tag13</code> is not being observed through:
        <ul>
          <li>normal <code class="language-plaintext highlighter-rouge">adsAddScene()</code> birth on the expected ACTIVITY startup path</li>
          <li>normal trigger handoff</li>
          <li>normal <code class="language-plaintext highlighter-rouge">ADS_THREAD_TERMINATED</code></li>
          <li>normal early authored <code class="language-plaintext highlighter-rouge">PURGE</code> / <code class="language-plaintext highlighter-rouge">UPDATE</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>either the startup launch request is not reaching the expected <code class="language-plaintext highlighter-rouge">adsAddScene(slot1, tag12/13)</code> call at all on PS1</li>
      <li>or <code class="language-plaintext highlighter-rouge">ps1AdsCurrentName/Tag</code> is already no longer the expected ACTIVITY context by the time the launch is born</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move one layer earlier than thread birth</li>
      <li>instrument the exact launch request state before <code class="language-plaintext highlighter-rouge">adsAddScene()</code> is called:
        <ul>
          <li>requested ADS family/tag</li>
          <li>current chunk slot/tag being executed</li>
          <li>requested <code class="language-plaintext highlighter-rouge">ttmSlotNo</code> / <code class="language-plaintext highlighter-rouge">ttmTag</code></li>
          <li>whether the ACTIVITY startup launch is being rewritten before thread creation</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1403-pdt">2026-03-28 14:03 PDT</h2>

<ul>
  <li>Added an even earlier callsite trap in <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code> for authored startup launches:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ADD_SCENE slot1 tag12/tag13</code></li>
      <li><code class="language-plaintext highlighter-rouge">ADD_SCENE_LOCAL slot1 tag12/tag13</code></li>
      <li>trap payload includes:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame</code></li>
          <li>requested <code class="language-plaintext highlighter-rouge">slot/tag/arg3</code></li>
          <li><code class="language-plaintext highlighter-rouge">inRandBlock</code></li>
          <li><code class="language-plaintext highlighter-rouge">inSkipBlock</code></li>
          <li>current trigger slot/tag</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Reran the exact scene:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-addscene-callsite-trap</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the callsite trap did <strong>not</strong> fire</li>
      <li>stable run still completed through <code class="language-plaintext highlighter-rouge">6300</code></li>
      <li>no <code class="language-plaintext highlighter-rouge">ACT1 add_scene_chunk</code> or <code class="language-plaintext highlighter-rouge">ACT1 add_scene_local</code> line appeared in logs</li>
    </ul>
  </li>
  <li>This strengthens the earlier negative traps:
    <ul>
      <li>on the stable PS1 path, the authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE slot1 tag12/tag13</code> opcodes are not being observed at all</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1405-pdt">2026-03-28 14:05 PDT</h2>

<ul>
  <li>Added a one-shot trap immediately after <code class="language-plaintext highlighter-rouge">adsLoad()</code> inside <code class="language-plaintext highlighter-rouge">adsPlay()</code> for:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">adsResource-&gt;resName == ACTIVITY.ADS</code></li>
      <li><code class="language-plaintext highlighter-rouge">adsTag == 1</code></li>
      <li>trap payload would have printed:
        <ul>
          <li>selected <code class="language-plaintext highlighter-rouge">offset</code></li>
          <li><code class="language-plaintext highlighter-rouge">dataSize</code></li>
          <li><code class="language-plaintext highlighter-rouge">numTags</code></li>
          <li>first three opcodes at the chosen entry point</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Reran the exact scene:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-adsload-offset-trap</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the <code class="language-plaintext highlighter-rouge">adsLoad()</code> trap did <strong>not</strong> fire</li>
      <li>run still exited cleanly with 41 captured frames</li>
      <li>no <code class="language-plaintext highlighter-rouge">ACT1 adsLoad ...</code> line appeared in logs</li>
    </ul>
  </li>
  <li>Current strongest read:
    <ul>
      <li>on the stable PS1 run, we are not even entering <code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 1)</code> on the path we thought we were</li>
      <li>so the bug surface has moved out of ADS execution and back into the boot override / story dispatch path that is supposed to invoke <code class="language-plaintext highlighter-rouge">adsPlay</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument the story boot override path directly:
        <ul>
          <li>where <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> is parsed</li>
          <li>where that request is held/persisted</li>
          <li>and the exact site that is supposed to call <code class="language-plaintext highlighter-rouge">adsPlay(boot_ads_name, boot_tag)</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1411-pdt">2026-03-28 14:11 PDT</h2>

<ul>
  <li>Probed progressively earlier boot/dispatch sites for the same exact override:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">storySetBootScene("ACTIVITY.ADS", 1)</code></li>
      <li><code class="language-plaintext highlighter-rouge">storyPlay()</code> boot-scene selection for <code class="language-plaintext highlighter-rouge">storyBootAdsName/storyBootAdsTag</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1ApplyBootOverride()</code> token parse for <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code> after successful BOOTMODE read</li>
      <li><code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code> early-return branches (<code class="language-plaintext highlighter-rouge">CdSearchFile</code>, zero-size, <code class="language-plaintext highlighter-rouge">CdRead</code>, <code class="language-plaintext highlighter-rouge">CdReadSync</code>)</li>
    </ul>
  </li>
  <li>Trap strings for all of those probes are definitely present in the built executable and in the rebuilt disc snapshot:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">build-ps1/jcreborn.exe</code></li>
      <li>snapshot <code class="language-plaintext highlighter-rouge">disc/jcreborn.bin</code></li>
    </ul>
  </li>
  <li>The rebuilt disc snapshot also definitely contains the staged override string:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1 seed 1</code></li>
    </ul>
  </li>
  <li>Yet none of those early boot traps fire at runtime, and no existing <code class="language-plaintext highlighter-rouge">[BOOT] ...</code> prints appear in regtest logs either.</li>
  <li>Practical conclusion:
    <ul>
      <li>for very-early pre-graphics PS1 boot code, <code class="language-plaintext highlighter-rouge">printf</code>/<code class="language-plaintext highlighter-rouge">fatalError</code>-style probes are no longer a trustworthy validation surface under the current regtest path</li>
      <li>this does <strong>not</strong> invalidate the later scene harness, only the latest method of proving early-boot execution</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>switch early boot diagnostics away from print/fatal probes and onto a non-print side effect that can be observed later in the frame output or runtime mode</li>
      <li>use that to prove whether <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code> is actually being loaded/applied on PS1</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1448-pdt">2026-03-28 14:48 PDT</h2>

<ul>
  <li>Confirmed a critical code-path detail in <code class="language-plaintext highlighter-rouge">jc_reborn.c</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ps1HasBootOverridePending()</code> intentionally ignores <code class="language-plaintext highlighter-rouge">story ...</code> overrides</li>
      <li>so <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> always goes through the title path first</li>
      <li>only <code class="language-plaintext highlighter-rouge">ps1ReapplyBootOverride()</code> can preserve that request for later story dispatch</li>
    </ul>
  </li>
  <li>Tried three temporary late behavioral latches keyed on:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ps1BootDbgStoryAdsApplied</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1BootDbgAdsSig == ACTIVITY</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1BootDbgAdsTag == 1</code></li>
    </ul>
  </li>
  <li>Latch attempts:
    <ul>
      <li>temporary <code class="language-plaintext highlighter-rouge">storyPlay()</code> reroute to <code class="language-plaintext highlighter-rouge">BUILDING.ADS 1</code></li>
      <li>temporary <code class="language-plaintext highlighter-rouge">main()</code> reroute to bench mode</li>
      <li>temporary <code class="language-plaintext highlighter-rouge">main()</code> reroute to low-level <code class="language-plaintext highlighter-rouge">island ads BUILDING.ADS 1</code></li>
      <li>temporary <code class="language-plaintext highlighter-rouge">main()</code> hard early exit</li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-behavioral-latch/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-main-latch/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-main-latch-ads/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-main-exit-latch/result.json</code></li>
    </ul>
  </li>
  <li>Strongest result:
    <ul>
      <li>the <code class="language-plaintext highlighter-rouge">main()</code> hard-exit latch did <strong>not</strong> trigger</li>
      <li>the run still followed the usual long path and dumped the normal sampled frames</li>
      <li>so the <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> request is not surviving far enough to trip a post-reapply latch in <code class="language-plaintext highlighter-rouge">main()</code></li>
    </ul>
  </li>
  <li>Caveat:
    <ul>
      <li>2700-frame visual controls are weak because several different boots still look the same there</li>
      <li>but the hard-exit latch is not visual, and its failure is the strongest proof in this set</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the active bug is upstream of story dispatch and likely still inside the PS1 boot-override retention/reapply path</li>
      <li>specifically: <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code> may be read, but the parsed <code class="language-plaintext highlighter-rouge">story ads ...</code> request is not surviving into the later runtime state we expected</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect where story boot globals are cleared between <code class="language-plaintext highlighter-rouge">ps1ApplyBootOverride()</code> and the later <code class="language-plaintext highlighter-rouge">storyPlay()</code> path</li>
      <li>focus on <code class="language-plaintext highlighter-rouge">storyResetBootState()</code> / <code class="language-plaintext highlighter-rouge">storySetBoot*()</code> callsites and any startup path that may be wiping story override state after reapply</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1503-pdt">2026-03-28 15:03 PDT</h2>

<ul>
  <li>Replaced the old RAM-only post-title reapply with a second <code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code> after <code class="language-plaintext highlighter-rouge">loadTitleScreenEarly()</code> in <code class="language-plaintext highlighter-rouge">repo:/jc_reborn.c</code>.</li>
  <li>Validation:
    <ul>
      <li>old long failing probe:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quiettrace6300/result.json</code></li>
        </ul>
      </li>
      <li>new long probe with reload-after-title:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-reload-after-title-6300/result.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important result:
    <ul>
      <li>state hashes changed</li>
      <li>but sampled PNGs at <code class="language-plaintext highlighter-rouge">2700</code>, <code class="language-plaintext highlighter-rouge">6000</code>, <code class="language-plaintext highlighter-rouge">6150</code>, and <code class="language-plaintext highlighter-rouge">6300</code> remained pixel-identical to the old failing run</li>
    </ul>
  </li>
  <li>
    <p>So the reload changes internal state retention, but it is not yet a visible scene fix.</p>
  </li>
  <li>Then reran hard latches on the reload-after-title path:
    <ul>
      <li>top-of-<code class="language-plaintext highlighter-rouge">storyPlay()</code> boot-vars latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-story-loop-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">storyBootAdsName/storyBootAdsTag</code> ads branch latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boot-branch-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">storyBootSingleSceneIndex</code> hijack latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-single-index-latch/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">storyBootSceneIndex</code> hijack latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-scene-index-latch/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
      <li>non-null <code class="language-plaintext highlighter-rouge">bootScene = storyFindSceneByAds(...)</code> latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootscene-null-check/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>later post-selection / prepared-scene latches:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prepared-exit-latch/result.json</code></li>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-postselect-latch/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current narrow read:
    <ul>
      <li>with reload-after-title, the <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code> override now survives into <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>it reaches the ads boot branch</li>
      <li>it resolves a non-null <code class="language-plaintext highlighter-rouge">bootScene</code></li>
      <li>it is not being hijacked by <code class="language-plaintext highlighter-rouge">storyBootSingleSceneIndex</code> or <code class="language-plaintext highlighter-rouge">storyBootSceneIndex</code></li>
      <li>but the path still does not survive to the later post-selection / prepared-scene launch boundary</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the exact control flow between a non-null <code class="language-plaintext highlighter-rouge">bootScene</code> and the later post-selection / prepared-scene path</li>
      <li>this is now the narrowest unexplained edge for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1513-pdt">2026-03-28 15:13 PDT</h2>

<ul>
  <li>Probed deeper around the exact <code class="language-plaintext highlighter-rouge">bootScene -&gt; finalScene -&gt; storyPrepareSceneState()</code> handoff.</li>
  <li>Results:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">finalScene = bootScene</code> immediate latch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-finalscene-assign-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>top of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(ACTIVITY.ADS, 1)</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-preparestate-entry-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>after <code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene()</code> but before <code class="language-plaintext highlighter-rouge">adsInitIsland()</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-adsinitisland-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>after <code class="language-plaintext highlighter-rouge">adsInitIsland()</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsinitisland-latch/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>So:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> definitely survives through:
        <ul>
          <li>ads boot-branch lookup</li>
          <li><code class="language-plaintext highlighter-rouge">finalScene = bootScene</code></li>
          <li><code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(finalScene)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>A same-run sticky-latch attempt that tried to carry “boot scene resolved” state forward to the later post-prepare block did <strong>not</strong> trip:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-sticky-resolve-latch/result.json</code></li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the path definitely reaches and returns from <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code></li>
      <li>but the later post-prepare point is still not being observed with the same proof method</li>
      <li>so the remaining gap is now after island prep and before or during the later debug/state block inside <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument the immediate span after <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code> with a more explicit side effect than the prior return-style latch</li>
      <li>likely by storing a sticky runtime flag after <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> and validating it from a later safe point or overlay decode instead of relying on local early returns</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1534-pdt">2026-03-28 15:34 PDT</h2>

<ul>
  <li>The harness remains at practical full confidence for scene debugging.</li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> is still the active PS1 target, but the active bug surface has moved out of story routing and into the raft BMP load path during island prep.</p>
  </li>
  <li>Exact narrowing chain:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code> survives immediately before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootscene-nonnull-pre-prepare/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>the same latch after <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code> did not fire:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootscene-nonnull-post-prepare/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
      <li>skipping <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> made the later latch fire:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-skip-adsinitisland-post-prepare/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>so the first corrupting call is inside <code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
    </ul>
  </li>
  <li>Then narrowed inside <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> / <code class="language-plaintext highlighter-rouge">islandInit()</code>:
    <ul>
      <li>returning after screen-load/layer setup:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-cut-after-screenload/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>returning after raft setup / before <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-cut-before-backgrnd/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
      <li>returning immediately after <code class="language-plaintext highlighter-rouge">grLoadBmp(ttmSlot, 0, "MRAFT.BMP")</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-cut-after-mraft-load/result.json</code></li>
          <li>did not fire</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>So the first bad edge is the raft BMP load itself.</p>
  </li>
  <li>Then narrowed inside <code class="language-plaintext highlighter-rouge">grLoadBmpRAM("MRAFT.BMP")</code>:
    <ul>
      <li>forcing <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code> off the PSB fast path did not help:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-no-psb-cut-after-load/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
      <li>returning before any BMP-byte load did help:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-cut-before-bmp-load/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>returning immediately after <code class="language-plaintext highlighter-rouge">ps1_loadBmpData()</code> did not:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-cut-after-bmp-load/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>So the corrupting operation is in the raw BMP data load path, before any frame-install loop.</p>
  </li>
  <li>Final split on the BMP data loader:
    <ul>
      <li>replacing <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code> load with a same-sized dummy zeroed heap buffer:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-dummy-buffer-post-load-cut/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>replacing it with <code class="language-plaintext highlighter-rouge">ps1_loadRawFile("\\BMP\\MRAFT.BMP;1", ...)</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-rawfile-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the first bad operation for stable <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> is an actual PS1 CD-read path for <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code></li>
      <li>it is not:
        <ul>
          <li>story override retention</li>
          <li>scene dispatch</li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code> generally</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>PSB decoding specifically</li>
          <li>frame-install into the sprite slot</li>
          <li>assigning a heap buffer to <code class="language-plaintext highlighter-rouge">bmpResource-&gt;uncompressedData</code></li>
        </ul>
      </li>
      <li>it is specifically triggered by real CD-backed <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code> loading</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect/fix the low-level PS1 CD-read path used by <code class="language-plaintext highlighter-rouge">ps1_loadBmpData()</code> / <code class="language-plaintext highlighter-rouge">ps1_loadRawFile()</code></li>
      <li>likely around <code class="language-plaintext highlighter-rouge">CdRead</code> buffer handling / alignment / transfer semantics rather than scene logic</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1538-pdt">2026-03-28 15:38 PDT</h2>

<ul>
  <li>Continued narrowing the <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> failure after the raft-load boundary.</li>
  <li>
    <p>The new question was whether the bad edge is specific to <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code> or to any real CD-backed read at that point in scene prep.</p>
  </li>
  <li>Additional splits:
    <ul>
      <li>forcing <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code> off PSB and returning before any frame-install loop:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-no-psb-preloop-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
      <li>returning before any BMP-byte load at all:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-cut-before-bmp-load/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>returning immediately after <code class="language-plaintext highlighter-rouge">ps1_loadBmpData()</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-cut-after-bmp-load/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>So the first bad operation is definitely in the raw BMP data load path, before any frame metadata/surface install.</p>
  </li>
  <li>Then split the load path itself:
    <ul>
      <li>substitute a same-sized dummy zeroed heap buffer instead of any real read:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-dummy-buffer-post-load-cut/result.json</code></li>
          <li>fired</li>
        </ul>
      </li>
      <li>substitute <code class="language-plaintext highlighter-rouge">ps1_loadRawFile("\\BMP\\MRAFT.BMP;1", ...)</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-rawfile-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
      <li>substitute <code class="language-plaintext highlighter-rouge">ps1_loadRawFile("\\BMP\\BOAT.BMP;1", ...)</code> for the raft load:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boat-rawfile-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the active bug is no longer asset-specific to <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code></li>
      <li>it is a generic real CD-read hazard at that exact scene-prep point</li>
      <li>specifically:
        <ul>
          <li>any actual live PS1 CD-backed BMP read there corrupts later state</li>
          <li>a same-sized heap allocation without CD I/O does not</li>
        </ul>
      </li>
      <li>so the next fix target is not scene logic or BMP parsing; it is the PS1 CD read path / DMA / buffer handling semantics used by <code class="language-plaintext highlighter-rouge">ps1_loadBmpData()</code> and <code class="language-plaintext highlighter-rouge">ps1_loadRawFile()</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>test safer alternate CD read semantics around <code class="language-plaintext highlighter-rouge">CdRead</code> / <code class="language-plaintext highlighter-rouge">CdReadSync</code>, likely with a different buffering strategy or synchronization policy, then rerun <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1539-pdt">2026-03-28 15:39 PDT</h2>

<ul>
  <li>
    <p>Kept the same stable <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> proof surface and focused only on the low-level read semantics.</p>
  </li>
  <li>New discriminators:
    <ul>
      <li>substitute <code class="language-plaintext highlighter-rouge">ps1_loadRawFile("\\BMP\\BOAT.BMP;1", ...)</code> instead of the raft load:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boat-rawfile-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
      <li>substitute a static-bounce-buffer reader that does real <code class="language-plaintext highlighter-rouge">CdRead</code> into static storage, then <code class="language-plaintext highlighter-rouge">memcpy</code> to heap:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boat-bounce-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>this is not <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code>-specific</li>
      <li>this is not a “DMA directly into malloc’d heap buffer” bug</li>
      <li>the active failure is any real live <code class="language-plaintext highlighter-rouge">CdRead</code> at that scene-prep moment</li>
      <li>so the next target is the PS1 CD state/synchronization path itself:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">CdControl(CdlSetloc, ...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">CdRead(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">CdReadSync(...)</code></li>
          <li>possible interaction with current engine state / callbacks / timing</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Additional check:
    <ul>
      <li>inserting <code class="language-plaintext highlighter-rouge">cdromResetState()</code> immediately before the live special-case read:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boat-bounce-post-load-cut-cdreset/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Updated read:
    <ul>
      <li>this is not merely stale CD state left over from earlier boot/title work</li>
      <li>the next target is the live <code class="language-plaintext highlighter-rouge">CdRead</code>/<code class="language-plaintext highlighter-rouge">CdReadSync</code> semantics themselves rather than reset ordering</li>
    </ul>
  </li>
  <li>One more discriminator:
    <ul>
      <li>switching the special-case live read to the legacy <code class="language-plaintext highlighter-rouge">ps1_fopen("BMP\\BOAT.BMP", "rb")</code> / <code class="language-plaintext highlighter-rouge">ps1_fread(...)</code> whole-file path:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boat-ps1fopen-post-load-cut/result.json</code></li>
          <li>still bad</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>it is not a bug isolated to <code class="language-plaintext highlighter-rouge">ps1_streamRead()</code></li>
      <li>it is not a bug isolated to <code class="language-plaintext highlighter-rouge">ps1_loadRawFile()</code></li>
      <li>it is not a bug isolated to pack reads vs fallback reads</li>
      <li>it is not a bug isolated to DMA directly into heap</li>
      <li>it is any live CD-backed read path at that scene-prep moment</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop trying alternate live readers</li>
      <li>shift to an architectural fix:
        <ul>
          <li>prefetch/cache needed BMP bytes before entering the sensitive scene-prep window, or</li>
          <li>otherwise guarantee <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> island prep does not issue live CD reads there</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1608-pdt">2026-03-28 16:08 PDT</h2>

<ul>
  <li>Tried the architectural fix in two steps:
    <ul>
      <li>added island-support BMP priming (<code class="language-plaintext highlighter-rouge">MRAFT.BMP</code>, <code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code>, <code class="language-plaintext highlighter-rouge">HOLIDAY.BMP</code>) to <code class="language-plaintext highlighter-rouge">adsPrimeRestorePilotResources()</code> for any restore pilot that uses <code class="language-plaintext highlighter-rouge">ISLETEMP.SCR</code></li>
      <li>then moved priming earlier into <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code> and prearmed the pilot pack before priming</li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prefetch-6300/20260328-155810/frames/jcreborn/frame_06150.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prepprime-6300/20260328-160137/frames/jcreborn/frame_06150.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prearm-prime-6300/20260328-160314/frames/jcreborn/frame_06150.png</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>none of those three ACTIVITY 1 runs changed the late failure band</li>
      <li>frames <code class="language-plaintext highlighter-rouge">6000</code>, <code class="language-plaintext highlighter-rouge">6150</code>, and <code class="language-plaintext highlighter-rouge">6300</code> stayed on the same wrong ocean output</li>
      <li>so the naive “prime earlier” hypothesis is not sufficient by itself</li>
    </ul>
  </li>
  <li>Follow-up probe:
    <ul>
      <li>added a temporary <code class="language-plaintext highlighter-rouge">islandInit()</code> residency trap around <code class="language-plaintext highlighter-rouge">MRAFT.BMP</code></li>
      <li>short proof run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-mraft-fatalproof/result.json</code></li>
        </ul>
      </li>
      <li>this did <strong>not</strong> trip the late fatal by <code class="language-plaintext highlighter-rouge">3000</code> frames</li>
      <li>sampled visuals on that run were title/black rather than the old ocean proof surface</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the harness remains good enough for scene debugging</li>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> is still the active scene</li>
      <li>the early-prime fix did not solve the issue</li>
      <li>the next unresolved edge is no longer just “load MRAFT earlier”; it is the exact scene/boot context that exists when island prep is attempted</li>
    </ul>
  </li>
  <li>Immediate next target:
    <ul>
      <li>replace the temporary <code class="language-plaintext highlighter-rouge">islandInit()</code> residency trap with a cleaner proof of the active ADS/tag/context at island-prep time</li>
      <li>then determine whether the wrong-ocean path is entering island prep under the wrong scene identity, or whether the resource residency is still being lost before use</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1618-pdt">2026-03-28 16:18 PDT</h2>

<ul>
  <li>Replaced the failed overlay/decoder angle with a stronger runtime proof:
    <ul>
      <li>temporarily forced a distinctive <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code>-only island state in <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">night = 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">holiday = 3</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-nightproof-6300/20260328-161502/frames/jcreborn/frame_06150.png</code></li>
    </ul>
  </li>
  <li>Important result:
    <ul>
      <li>the old wrong bright-cyan day ocean at <code class="language-plaintext highlighter-rouge">6000/6150/6300</code> changed into a night-ocean variant</li>
      <li>so the visible bad path is definitely carrying the prepared <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> island state forward</li>
      <li>this rules out the hypothesis that the wrong-ocean branch is some unrelated post-title scene path ignoring <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> background/island state prep is being applied</li>
      <li>but the actual scene content / sprite-thread side is still not launching or composing correctly</li>
      <li>so the remaining bug is now narrower again:
        <ul>
          <li>not story boot routing</li>
          <li>not final-scene selection</li>
          <li>not island-state preparation</li>
          <li>still in the transition from prepared scene state into live ADS/TTM scene content</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary night/holiday proof patch after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>return to the clean <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> path and debug why prepared ACTIVITY background state survives while the ACTIVITY scene threads/content still collapse into empty ocean</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1621-pdt">2026-03-28 16:21 PDT</h2>

<ul>
  <li>Removed the remaining intrusive ACTIVITY-specific fatal traps from <code class="language-plaintext highlighter-rouge">ads.c</code> so the runtime proof surface is passive again:
    <ul>
      <li>zero-IP launch abort</li>
      <li>trigger abort</li>
      <li>startup-thread alive/terminated aborts</li>
      <li>slot-2 zero-IP handoff aborts</li>
    </ul>
  </li>
  <li>Clean validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-cleantelemetry-6300/20260328-161912/frames/jcreborn/frame_06150.png</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>removing those ACTIVITY-specific aborts did <strong>not</strong> change the visible scene behavior</li>
      <li>frame <code class="language-plaintext highlighter-rouge">6150</code> is back to the same bright day-ocean failure shape as before</li>
      <li>so the remaining bug is not an artifact of the old ACT1 fatal instrumentation</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>harness is still at practical full confidence</li>
      <li>the prepared-scene state can be proven to survive</li>
      <li>the cleaned runtime still collapses to empty ocean instead of real ACTIVITY scene content</li>
      <li>next target remains the live ADS/TTM content path rather than story boot or island-state prep</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1630-pdt">2026-03-28 16:30 PDT</h2>

<ul>
  <li>Added two temporary late proofs for the clean <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> path:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">story.c</code>, after <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code>, fatal if <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> returns with <code class="language-plaintext highlighter-rouge">ps1AdsLastPlayLaunched == 0</code></li>
      <li>in <code class="language-plaintext highlighter-rouge">ads.c</code>, inside the <code class="language-plaintext highlighter-rouge">adsPlay()</code> main loop, fatal if <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> is still running past frame <code class="language-plaintext highlighter-rouge">5400</code> with <code class="language-plaintext highlighter-rouge">ps1AdsLastPlayLaunched == 0</code></li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-launchproof-6300/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-adsloop-launchtrap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>neither proof fired</li>
      <li>both runs still landed on the same stable late-ocean failure band:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_05400</code> through <code class="language-plaintext highlighter-rouge">frame_06300</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">scene_markers_last.launched</code> remained <code class="language-plaintext highlighter-rouge">false</code> in both runs</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the visible bad path is not simply:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> returning immediately with no launch, or</li>
          <li><code class="language-plaintext highlighter-rouge">adsPlay()</code> staying live into the ocean band with no launch</li>
        </ul>
      </li>
      <li>so the remaining false-<code class="language-plaintext highlighter-rouge">launched</code> state is being reached through a different path than those two proof sites capture</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted both temporary proof guards after recording the negative result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move the proof surface away from those two sites and identify where the late ocean path is actually re-entering/retrying scene control while leaving <code class="language-plaintext highlighter-rouge">ps1AdsLastPlayLaunched</code> false</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1635-pdt">2026-03-28 16:35 PDT</h2>

<ul>
  <li>Tested the most obvious remaining re-entry hypothesis directly in <code class="language-plaintext highlighter-rouge">storyPlay()</code>:
    <ul>
      <li>temporary proof patch on the <code class="language-plaintext highlighter-rouge">if (!ps1AdsLastPlayLaunched)</code> retry branch</li>
      <li>for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, force a visible island-state mutation there:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">night = 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">holiday = 3</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-retrybranch-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late <code class="language-plaintext highlighter-rouge">5400–6300</code> ocean frames did <strong>not</strong> change at all</li>
      <li>same day-ocean output</li>
      <li>same <code class="language-plaintext highlighter-rouge">state_hash</code></li>
      <li>same false <code class="language-plaintext highlighter-rouge">launched</code> marker</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the stable late-ocean band is not coming from the obvious <code class="language-plaintext highlighter-rouge">storyPlay()</code> failed-launch retry branch either</li>
      <li>combined with the earlier negative proofs, the bad path is bypassing:
        <ul>
          <li>the post-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> failed-launch branch</li>
          <li>the late <code class="language-plaintext highlighter-rouge">adsPlay()</code> in-loop no-launch proof</li>
          <li>the obvious <code class="language-plaintext highlighter-rouge">storyPlay()</code> failed-launch retry branch side effect</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary retry-branch proof patch after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move one layer outside of <code class="language-plaintext highlighter-rouge">storyPlay()</code> control-flow assumptions and identify what code path is drawing the late ocean band while all three expected no-launch branches fail to explain it</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1640-pdt">2026-03-28 16:40 PDT</h2>

<ul>
  <li>Tested whether the late ACTIVITY ocean band was being sustained by the persistent island background thread itself:
    <ul>
      <li>temporary proof patch in <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code></li>
      <li>for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, call <code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code> immediately after <code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-noislandbg-proof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>no change at all</li>
      <li>same late ocean frames</li>
      <li>same <code class="language-plaintext highlighter-rouge">state_hash</code></li>
      <li>same false <code class="language-plaintext highlighter-rouge">launched</code> marker</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the late bad band is not being sustained by the background/wave thread</li>
      <li>it is a static ocean background already baked into <code class="language-plaintext highlighter-rouge">grBackgroundSfc</code> during island prep</li>
      <li>the real missing piece is still the ACTIVITY scene content layer, not the island background thread lifetime</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code> proof patch after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>focus directly on why ACTIVITY scene content never composites over that already-prepared ocean background</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1645-pdt">2026-03-28 16:45 PDT</h2>

<ul>
  <li>Proved the late ACTIVITY ocean band is not receiving live sprite draws at all:
    <ul>
      <li>temporary fatal traps added to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">grDrawSprite()</code></li>
          <li><code class="language-plaintext highlighter-rouge">grDrawSpriteFlip()</code></li>
        </ul>
      </li>
      <li>only for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after frame <code class="language-plaintext highlighter-rouge">5400</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-latedraw-trap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>neither draw trap fired</li>
      <li>the run still reached the same stable late-ocean band unchanged</li>
      <li>so by <code class="language-plaintext highlighter-rouge">5400–6300</code>, no ACTIVITY scene sprite draw is reaching the renderer at all</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the missing ACTIVITY content layer is failing before <code class="language-plaintext highlighter-rouge">grDrawSprite()</code> / <code class="language-plaintext highlighter-rouge">grDrawSpriteFlip()</code></li>
      <li>the ocean band is just the prepared static background with no live content ops arriving in the late window</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late draw traps after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move one layer earlier than rendering:
        <ul>
          <li>determine why <code class="language-plaintext highlighter-rouge">ttmPlay()</code> for the supposed ACTIVITY content is no longer issuing <code class="language-plaintext highlighter-rouge">DRAW_SPRITE</code> opcodes by the late window</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1650-pdt">2026-03-28 16:50 PDT</h2>

<ul>
  <li>Moved one layer earlier than rendering and proved no ACTIVITY opcode execution reaches the late window either:
    <ul>
      <li>temporary fatal trap at the entry of <code class="language-plaintext highlighter-rouge">ttmPlay()</code></li>
      <li>only for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after frame <code class="language-plaintext highlighter-rouge">5400</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-latettmp-trap/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the <code class="language-plaintext highlighter-rouge">ttmPlay()</code> trap did <strong>not</strong> fire</li>
      <li>the run still reached the same stable late-ocean band unchanged</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>by <code class="language-plaintext highlighter-rouge">5400–6300</code>, no ACTIVITY thread is even reaching opcode execution anymore</li>
      <li>combined with the earlier late draw proof, the ACTIVITY content layer is dead before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ttmPlay()</code></li>
          <li><code class="language-plaintext highlighter-rouge">grDrawSprite()</code></li>
          <li><code class="language-plaintext highlighter-rouge">grDrawSpriteFlip()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">ttmPlay()</code> trap after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move earlier again to the thread lifecycle itself:
        <ul>
          <li>why the ACTIVITY scene threads have vanished before the late window while the prepared static ocean background remains on screen</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1659-pdt">2026-03-28 16:59 PDT</h2>

<ul>
  <li>Added a sticky <code class="language-plaintext highlighter-rouge">ps1StoryDbgBootReturnSeen</code> proof bit for the <code class="language-plaintext highlighter-rouge">if (bootScene != NULL) { ... return; }</code> success-return branch in <code class="language-plaintext highlighter-rouge">storyPlay()</code>.</li>
  <li>First attempted to expose that bit in the top-left drop panel, but that surface was contaminated by unrelated lit pixels earlier in the run and was not trustworthy.</li>
  <li>
    <p>Moved the same proof bit into the compact scene-identity strip and decoded it directly on the late ACTIVITY frame.</p>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootreturn-proof/result.json</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootreturn-compactproof/result.json</code></li>
    </ul>
  </li>
  <li>Clean result:
    <ul>
      <li>at <code class="language-plaintext highlighter-rouge">frame_06150</code>, <code class="language-plaintext highlighter-rouge">boot_return_seen_estimate = 0</code></li>
      <li>so the late ocean band is <strong>not</strong> post-return residue from the <code class="language-plaintext highlighter-rouge">bootScene != NULL</code> success path</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the stable late bad band is still being reached before the boot-scene success-return branch</li>
      <li>that rules out one of the last obvious “scene already ended and left its background behind” explanations</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1702-pdt">2026-03-28 17:02 PDT</h2>

<ul>
  <li>Added sticky ACTIVITY attempt counters:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ps1StoryDbgActivityPrepareCount</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1StoryDbgActivityPlayPreparedCount</code></li>
    </ul>
  </li>
  <li>
    <p>The compact-strip decode for those counters was too noisy to trust numerically, so I switched back to a behavioral proof.</p>
  </li>
  <li>Behavioral proof:
    <ul>
      <li>only on the <strong>second</strong> <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(ACTIVITY.ADS, 1)</code> call, forced <code class="language-plaintext highlighter-rouge">islandState.night = 1</code> and <code class="language-plaintext highlighter-rouge">holiday = 3</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-second-prepare-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late <code class="language-plaintext highlighter-rouge">5400–6300</code> ocean stayed the same bright day ocean</li>
      <li>no night mutation appeared</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the stable bad path is not repeatedly re-preparing <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> before the late window</li>
      <li>current best read is now:
        <ul>
          <li>a single ACTIVITY scene-prep/background-prep pass survives</li>
          <li>live ACTIVITY content still dies before late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> and late draw</li>
          <li>and the engine has not yet taken the normal boot-scene success-return path by the late sampled window</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary second-prepare nightproof after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>prove whether <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> is being entered on the stable ACTIVITY path in the current clean runtime, using a similarly late-safe behavioral proof rather than a noisy counter decode</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1706-pdt">2026-03-28 17:06 PDT</h2>

<ul>
  <li>Tried a simpler ACTIVITY entry proof by exposing sticky booleans for:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">storyPrepareSceneState(ACTIVITY.ADS, 1)</code> seen</li>
      <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> seen</li>
    </ul>
  </li>
  <li>
    <p>The compact-strip decode for those added rows still overlapped other overlay content and was not reliable enough to trust numerically.</p>
  </li>
  <li>Switched back to the behavior surface that already proved reliable:
    <ul>
      <li>temporary mutation at the top of <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code>:
        <ul>
          <li>force <code class="language-plaintext highlighter-rouge">night/holiday</code></li>
          <li>rebuild island via <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsInitIsland();</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-playprepared-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>late <code class="language-plaintext highlighter-rouge">5400–6300</code> output stayed the same bright day ocean</li>
      <li>no night mutation appeared</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>on the stable clean ACTIVITY failure path, the renderer-visible late ocean is not coming through <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code></li>
      <li>combined with the earlier proofs:
        <ul>
          <li>it is not a repeated second prepare</li>
          <li>it is not the boot-scene success-return residue</li>
          <li>it is not late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> / late draw activity</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the bad ACTIVITY path is diverging before the normal prepared-scene playback function</li>
      <li>but after enough ACTIVITY-specific scene preparation/background work has already happened to leave the static ocean in place</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> nightproof after recording the result</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>identify the pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> path that can leave ACTIVITY-prepared ocean background on screen while never entering the normal prepared-scene playback path</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1709-pdt">2026-03-28 17:09 PDT</h2>

<ul>
  <li>Tested whether the stable bad ACTIVITY path is still using the resolved <code class="language-plaintext highlighter-rouge">bootScene != NULL</code> / <code class="language-plaintext highlighter-rouge">finalScene</code> preparation path at all.</li>
  <li>Temporary behavioral proof:
    <ul>
      <li>immediately after <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>, only when:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li><code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS tag 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">finalScene</code> is an island scene</li>
        </ul>
      </li>
      <li>force <code class="language-plaintext highlighter-rouge">night/holiday</code></li>
      <li>rebuild island via <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsInitIsland();</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootscene-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>late <code class="language-plaintext highlighter-rouge">5400–6300</code> output stayed the same bright day ocean</li>
      <li>no night mutation appeared</li>
    </ul>
  </li>
  <li>Important narrowing:
    <ul>
      <li>the stable bad ACTIVITY path is not the normal resolved <code class="language-plaintext highlighter-rouge">bootScene/finalScene</code> preparation path either</li>
      <li>combined with earlier proofs, the visible late ocean is now outside all of these expected sites:
        <ul>
          <li>boot-scene success return</li>
          <li>repeated second prepare</li>
          <li>normal <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code></li>
          <li>normal resolved <code class="language-plaintext highlighter-rouge">bootScene/finalScene</code> island-prep mutation point</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">bootScene/finalScene</code> nightproof after recording the result</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the late ACTIVITY bad band is being reached through an earlier or parallel path that still leaves an ACTIVITY-like prepared ocean background in place, but bypasses the normal boot-scene/final-scene playback and return surfaces we have tested</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>step out of <code class="language-plaintext highlighter-rouge">story.c</code> proofs and instrument who is writing the late ocean background surface (<code class="language-plaintext highlighter-rouge">grBackgroundSfc</code>) in the ACTIVITY failure path, since the control-flow proofs in the normal scene path are now mostly exhausted</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1715-pdt">2026-03-28 17:15 PDT</h2>

<ul>
  <li>Switched proof surface from <code class="language-plaintext highlighter-rouge">story.c</code> control flow to the actual background writer in <code class="language-plaintext highlighter-rouge">island.c</code>.</li>
  <li>Added a helper in <code class="language-plaintext highlighter-rouge">story.c</code> / <code class="language-plaintext highlighter-rouge">repo:/story.h</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">storyBootOverrideMatches(const char *adsName, uint16 adsTag)</code></li>
    </ul>
  </li>
  <li>First writer proof:
    <ul>
      <li>only under boot override <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, force <code class="language-plaintext highlighter-rouge">islandState.night = 1</code>, <code class="language-plaintext highlighter-rouge">holiday = 3</code> at the top of <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-islandinit-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late bad band changed completely</li>
      <li>state hash changed from the old stable ocean hash</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the late ACTIVITY failure surface is definitely being produced through <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this is not just stale framebuffer residue or a path entirely outside island background setup</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1717-pdt">2026-03-28 17:17 PDT</h2>

<ul>
  <li>Narrowed <code class="language-plaintext highlighter-rouge">islandInit()</code> one more step:
    <ul>
      <li>for boot override <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, return immediately after the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
      <li>skip all later raft/cloud/island compositing in <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-islandinit-loadonly-proof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late <code class="language-plaintext highlighter-rouge">5400–6300</code> bad band stayed <strong>exactly the same</strong> as the original stable failure:
        <ul>
          <li>same state hash</li>
          <li>same bright day-ocean frames</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the later raft/cloud/island compositing in <code class="language-plaintext highlighter-rouge">islandInit()</code> is <strong>not</strong> required for the stable ACTIVITY failure</li>
      <li>the initial <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code> done by <code class="language-plaintext highlighter-rouge">islandInit()</code> is sufficient by itself to produce the late bad surface</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the stable late ACTIVITY failure is now tightly narrowed to the ocean screen load path itself</li>
      <li>the wrong surface is being baked in at <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code>, before later island composition or normal scene playback ever matters</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument why <code class="language-plaintext highlighter-rouge">islandInit()</code> is reaching the <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> load on the ACTIVITY failure path, and whether the intended ACTIVITY-specific screen/background choice is ever being selected before that fallback ocean load happens</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1720-pdt">2026-03-28 17:20 PDT</h2>

<ul>
  <li>Added a direct ACTIVITY ADS-entry proof in <code class="language-plaintext highlighter-rouge">adsPlay()</code>:
    <ul>
      <li>while testing, if <code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 1)</code> is entered, force <code class="language-plaintext highlighter-rouge">night/holiday</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-adsplay-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late <code class="language-plaintext highlighter-rouge">5400–6300</code> bad band did <strong>not</strong> change at all</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable late ACTIVITY failure is not coming through <code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 1)</code> entry itself</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1722-pdt">2026-03-28 17:22 PDT</h2>

<ul>
  <li>Then tested a broader story-level hypothesis:
    <ul>
      <li>while boot override <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> is still pending,</li>
      <li>mutate <strong>any</strong> island <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(scene)</code> call to <code class="language-plaintext highlighter-rouge">night/holiday</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-any-prepare-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late bad band changed completely</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable late failure is still reaching some island-scene preparation while the ACTIVITY override remains pending</li>
      <li>but it is not necessarily the ACTIVITY scene itself</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1723-pdt">2026-03-28 17:23 PDT</h2>

<ul>
  <li>Narrowed that branch again:
    <ul>
      <li>mutate only when:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li><code class="language-plaintext highlighter-rouge">storyBootOverrideMatches("ACTIVITY.ADS", 1)</code></li>
          <li><code class="language-plaintext highlighter-rouge">finalScene-&gt;flags &amp; ISLAND</code></li>
        </ul>
      </li>
      <li>before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bootscene-null-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the late bad band did <strong>not</strong> change</li>
    </ul>
  </li>
  <li>Important read:
    <ul>
      <li>the stable path is not explained by the simple <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> random-final-scene branch alone</li>
      <li>but it also is not entering <code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 1)</code> directly</li>
      <li>and <code class="language-plaintext highlighter-rouge">story ads ...</code> is confirmed to still route through <code class="language-plaintext highlighter-rouge">storyPlay()</code>, not <code class="language-plaintext highlighter-rouge">storyPlayBootSceneDirect()</code></li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the remaining control-flow suspect is now the early PS1 title/boot gating in <code class="language-plaintext highlighter-rouge">jc_reborn.c</code>, especially:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1HasBootOverridePending()</code></li>
          <li>override reload/application timing around title startup</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>test whether allowing <code class="language-plaintext highlighter-rouge">story ads</code> to count as a pending boot override changes the ACTIVITY failure shape, since that is now the cleanest unresolved gating edge</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1729-pdt">2026-03-28 17:29 PDT</h2>

<ul>
  <li>Tested the early PS1 title/boot gating hypothesis directly in <code class="language-plaintext highlighter-rouge">repo:/jc_reborn.c</code>:
    <ul>
      <li>changed <code class="language-plaintext highlighter-rouge">ps1HasBootOverridePending()</code> so <code class="language-plaintext highlighter-rouge">story ...</code> overrides count as pending too</li>
      <li>this suppresses the early title preload gate for <code class="language-plaintext highlighter-rouge">story ads ACTIVITY.ADS 1</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pendinggate-proof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>this does change the early shape materially:
        <ul>
          <li>earlier frames that used to show title now go black sooner</li>
          <li>the late ocean starts earlier (<code class="language-plaintext highlighter-rouge">04950</code> instead of <code class="language-plaintext highlighter-rouge">05400</code>)</li>
        </ul>
      </li>
      <li>but the stable late bad band is still the same ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749f...</code></li>
        </ul>
      </li>
      <li>and host compare is still a hard mismatch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/activity1-pendinggate-vs-host.json</code></li>
          <li>verdict still <code class="language-plaintext highlighter-rouge">PIXEL_MISMATCH</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Comparison to old clean baseline:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/activity1-old-vs-host.json</code></li>
      <li>host mismatch is unchanged in substance</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the title/boot gate is a real factor in startup shape</li>
      <li>but it is not the root fix for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
      <li>the remaining bug still lies later, in how the pending ACTIVITY override interacts with subsequent scene preparation / scene selection after startup</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">story ads</code> should probably count as a pending boot override for control-flow correctness</li>
      <li>but even with that fix, the ACTIVITY path still falls into the same wrong late ocean state</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>keep the pending-gate result in mind, but continue tracing the later handoff between startup and island preparation, because that is still where ACTIVITY correctness is lost</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1736-pdt">2026-03-28 17:36 PDT</h2>

<ul>
  <li>Switched from branch proofs back to family proofs inside <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(scene)</code> while the pending override is still <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>.</li>
  <li>First tested <code class="language-plaintext highlighter-rouge">WALKSTUF.ADS</code> only:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-walkstuf-prepare-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Then tested <code class="language-plaintext highlighter-rouge">FISHING.ADS</code> only:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fishing-prepare-nightproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>neither changed the stable late ocean band</li>
      <li>both preserved the same ocean hash <code class="language-plaintext highlighter-rouge">59cd749f...</code></li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earlier “any island prepare while ACTIVITY override is pending” proof is not explained by either:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">WALKSTUF.ADS</code> island prep</li>
          <li><code class="language-plaintext highlighter-rouge">FISHING.ADS</code> island prep</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>either another island family is being prepared under the still-pending ACTIVITY override</li>
      <li>or the broader “any prepare” proof is interacting with shared state in a way that is not family-local</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary family-specific proof patch after recording both results</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move away from heuristic family guesses and identify the exact prepared scene family/tag through a more deterministic state surface, rather than guessing by visual classifier labels</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1744-pdt">2026-03-28 17:44 PDT</h2>

<ul>
  <li>Added exact prepared-scene debug fields in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ps1StoryDbgPreparedSceneAdsSig</code></li>
      <li><code class="language-plaintext highlighter-rouge">ps1StoryDbgPreparedSceneTag</code></li>
    </ul>
  </li>
  <li>
    <p>First tried exposing them in the legacy story counter-bar panel in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code> and decoding them via <code class="language-plaintext highlighter-rouge">repo:/scripts/decode-ps1-bars.py</code>.</p>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-preparedtrace/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the legacy story panel rows saturated to full width (<code class="language-plaintext highlighter-rouge">90</code>) on the late frames</li>
      <li>so that panel is not a trustworthy decode surface for exact prepared-scene identity</li>
    </ul>
  </li>
  <li>Follow-up:
    <ul>
      <li>moved the same prepared-scene fields into the compact white-bit scene-marker strip in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code></li>
      <li>extended <code class="language-plaintext highlighter-rouge">repo:/scripts/decode-ps1-bars.py</code> to decode:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">prepared_scene_ads_family_estimate</code></li>
          <li><code class="language-plaintext highlighter-rouge">prepared_scene_tag_estimate</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation rerun:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-preparedstrip/result.json</code></li>
    </ul>
  </li>
  <li>Late-frame decode on the compact strip:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: <code class="language-plaintext highlighter-rouge">prepared=0/0</code>, <code class="language-plaintext highlighter-rouge">requested=63/0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">prepared=0/0</code>, <code class="language-plaintext highlighter-rouge">requested=63/0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: <code class="language-plaintext highlighter-rouge">prepared=0/0</code>, <code class="language-plaintext highlighter-rouge">requested=63/0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the new prepared-scene field is consistent with “no prepared scene identity survives into the late ocean band”</li>
      <li>but the adjacent requested-family bits are still saturating (<code class="language-plaintext highlighter-rouge">63</code>) on the same compact surface</li>
      <li>so the compact strip is not yet clean enough to treat the exact family/tag values as final truth</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the ACTIVITY failure still looks like “prepared/background state without surviving scene playback”</li>
      <li>and the next barrier is now the proof surface itself: we need one exact scene-identity signal that is robust under late-frame scaling/filtering, not another branch guess</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>switch from compact multi-bit family/tag encoding to a simpler, one-hot or low-cardinality exact prepared-scene proof surface that cannot saturate into ambiguous <code class="language-plaintext highlighter-rouge">63</code> values under DuckStation scaling</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1751-pdt">2026-03-28 17:51 PDT</h2>

<ul>
  <li>Replaced the ambiguous compact prepared-scene family/tag decode with one-hot prepared-family rows in the robust scene-marker strip:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">prepared_activity1</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_activity</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_building</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_fishing</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_johnny</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_mary</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_miscgag</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_stand</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_suzy</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_visitor</code></li>
      <li><code class="language-plaintext highlighter-rouge">prepared_walkstuf</code></li>
    </ul>
  </li>
  <li>Validation rerun:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-preparedfamily/result.json</code></li>
    </ul>
  </li>
  <li>Late-frame decode:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: all prepared-family one-hots <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: all prepared-family one-hots <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: all prepared-family one-hots <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
    </ul>
  </li>
  <li>Whole sampled run scan:
    <ul>
      <li>the one-hot family rows only light during the early noisy startup frames (<code class="language-plaintext highlighter-rouge">00300</code>, <code class="language-plaintext highlighter-rouge">00450</code>, <code class="language-plaintext highlighter-rouge">00600</code>), where they all saturate simultaneously and are not trustworthy</li>
      <li>after that, no prepared-family row lights again</li>
      <li><code class="language-plaintext highlighter-rouge">boot_pending</code> remains asserted from <code class="language-plaintext highlighter-rouge">04950</code> through <code class="language-plaintext highlighter-rouge">06300</code></li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>on the stable ACTIVITY failure path, there is no trustworthy evidence that <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code> reaches any concrete island family during the late bad window</li>
      <li>the bug target moves earlier again:
        <ul>
          <li>the override remains pending</li>
          <li>but stable scene preparation is not being reached on the path that leads to the late ocean band</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the late ocean band is still real and stable</li>
      <li>but it is no longer defensible to describe it as “late prepared scene X”</li>
      <li>instead, the stronger statement is:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">boot_pending</code> survives</li>
          <li>normal prepared-scene identity does not</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop trying to identify a late prepared family</li>
      <li>instrument the handoff that should consume the pending override and enter <code class="language-plaintext highlighter-rouge">storyPrepareSceneState()</code> on the stable path, because that handoff is now the narrowest unexplained edge</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1757-pdt">2026-03-28 17:57 PDT</h2>

<ul>
  <li>Added sticky handoff markers for the <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> pending-override path in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">pending_boot_resolved</code></li>
      <li><code class="language-plaintext highlighter-rouge">pending_prepare</code></li>
      <li><code class="language-plaintext highlighter-rouge">pending_play_prepared</code></li>
      <li><code class="language-plaintext highlighter-rouge">pending_play_prepared_returned</code></li>
    </ul>
  </li>
  <li>
    <p>Exposed those as robust white-block rows in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code> and decoded them in <code class="language-plaintext highlighter-rouge">repo:/scripts/decode-ps1-bars.py</code>.</p>
  </li>
  <li>Validation rerun:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pendinghandoff/result.json</code></li>
    </ul>
  </li>
  <li>Late-frame decode:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: all handoff markers <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: all handoff markers <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: all handoff markers <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
    </ul>
  </li>
  <li>Whole sampled run scan:
    <ul>
      <li>all four sticky handoff markers light only during the early noisy startup frames (<code class="language-plaintext highlighter-rouge">00300</code>, <code class="language-plaintext highlighter-rouge">00450</code>, <code class="language-plaintext highlighter-rouge">00600</code>)</li>
      <li>after that, none of them light again</li>
      <li><code class="language-plaintext highlighter-rouge">boot_pending</code> remains asserted from <code class="language-plaintext highlighter-rouge">04950</code> through <code class="language-plaintext highlighter-rouge">06300</code></li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>on the stable ACTIVITY failure path, there is still no trustworthy evidence that the pending override reaches:
        <ul>
          <li>resolved boot-scene handoff</li>
          <li>stable prepare entry</li>
          <li>stable play-prepared entry</li>
          <li>stable play-prepared return</li>
        </ul>
      </li>
      <li>the only late surviving signal is still the pending override itself</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the stable failure path is earlier than the normal scene handoff/control-flow surfaces we have been instrumenting</li>
      <li>the early startup/title window is still polluting these markers, but the late window is now consistent:
        <ul>
          <li>pending override survives</li>
          <li>no later stable handoff marker survives with it</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop relying on markers that can be polluted by the early startup samples</li>
      <li>instrument a later-safe state transition that only becomes writable after intro/title flow is complete, so we can distinguish “never consumed” from “consumed then reset”</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1801-pdt">2026-03-28 18:01 PDT</h2>

<ul>
  <li>Added a late-safe gate to the sticky pending-handoff markers in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>:
    <ul>
      <li>the ACTIVITY handoff latches now only set after <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
    </ul>
  </li>
  <li>Validation rerun:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pendinghandoff-late/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the sampled late frames still show only:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
        </ul>
      </li>
      <li>all of these remain <code class="language-plaintext highlighter-rouge">0</code> at <code class="language-plaintext highlighter-rouge">04950</code> through <code class="language-plaintext highlighter-rouge">06300</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">pending_boot_resolved</code></li>
          <li><code class="language-plaintext highlighter-rouge">pending_prepare</code></li>
          <li><code class="language-plaintext highlighter-rouge">pending_play_prepared</code></li>
          <li><code class="language-plaintext highlighter-rouge">pending_play_prepared_returned</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>gating those markers behind a later frame threshold did not change the core read</li>
      <li>the stable ACTIVITY failure path still preserves only the pending override</li>
      <li>it does not preserve any trustworthy evidence of the later normal handoff surfaces</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the unexplained edge is now even narrower:
        <ul>
          <li>the override remains pending after startup</li>
          <li>but the code path that should consume it into normal scene handoff still leaves no late stable trace</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move one layer lower than the current story markers and find a later-safe “override consumed” state that cannot be confused with startup/title replay</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1806-pdt">2026-03-28 18:06 PDT</h2>

<ul>
  <li>Split the late pending-override source further in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">late_boot_ads_exact</code></li>
      <li><code class="language-plaintext highlighter-rouge">late_boot_lookup_null</code></li>
      <li><code class="language-plaintext highlighter-rouge">late_boot_lookup_found</code></li>
    </ul>
  </li>
  <li>
    <p>Exposed those as late sticky rows in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code> and decoded them in <code class="language-plaintext highlighter-rouge">repo:/scripts/decode-ps1-bars.py</code>.</p>
  </li>
  <li>Validation rerun:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-latebootsource/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>after startup, from <code class="language-plaintext highlighter-rouge">04950</code> through <code class="language-plaintext highlighter-rouge">06300</code>, the only surviving late signal is still:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">boot_pending=1</code></li>
        </ul>
      </li>
      <li>all of these remain <code class="language-plaintext highlighter-rouge">0</code> in the late window:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">late_boot_ads_exact</code></li>
          <li><code class="language-plaintext highlighter-rouge">late_boot_lookup_null</code></li>
          <li><code class="language-plaintext highlighter-rouge">late_boot_lookup_found</code></li>
          <li><code class="language-plaintext highlighter-rouge">pending_boot_resolved</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the new split markers still only light in the polluted early startup samples and do not survive into the late ACTIVITY failure path</li>
      <li>so they do not change the late-path read:
        <ul>
          <li>stable late evidence still shows only an unresolved pending override</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the late ocean band is downstream of “override still pending”</li>
      <li>but upstream of every stable story-level resolution/prepare/play marker we have made durable so far</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop extending the same story overlay family of markers</li>
      <li>move to a different proof surface entirely for the next cut, because the current late-safe story markers are no longer adding new separation</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1811-pdt">2026-03-28 18:11 PDT</h2>

<ul>
  <li>Switched from passive markers to a behavioral proof in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>:
    <ul>
      <li>if the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending after frame <code class="language-plaintext highlighter-rouge">1000</code>,
clear it immediately via <code class="language-plaintext highlighter-rouge">storyClearBootSceneOverrideOnly()</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-lateclear/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the stable late output did not change at all</li>
      <li>the late frame hash is byte-identical to the previous late-handoff run:
        <ul>
          <li>old <code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li>new <code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the lingering ACTIVITY boot override is not the active cause of the late ocean band</li>
      <li>it is residue</li>
      <li>the real failure has already happened earlier by the time the late window is reached</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late-clear proof patch after validating the identical output</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the active bug is now clearly upstream of the surviving <code class="language-plaintext highlighter-rouge">boot_pending</code> residue</li>
      <li>continuing to chase the late pending-override state itself will not produce the fix</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>shift the proof surface from late residual story state to the earlier point where the ACTIVITY path first diverges into the wrong ocean outcome</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1815-pdt">2026-03-28 18:15 PDT</h2>

<ul>
  <li>Ran a finer-grained ACTIVITY boundary scan with 30-frame sampling:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boundary5100/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>on the current build, the stable wrong-ocean hash first appears at:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04920</code></li>
        </ul>
      </li>
      <li>before that, the run is still changing through several distinct earlier states</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the useful instrumentation window is now much tighter</li>
      <li>there is no need to keep probing the broad <code class="language-plaintext highlighter-rouge">5000+</code> late band</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the ACTIVITY failure becomes stable at <code class="language-plaintext highlighter-rouge">04920</code></li>
      <li>so the next debug cut should target the transition band immediately before that:
        <ul>
          <li>roughly <code class="language-plaintext highlighter-rouge">04800–04920</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument the first bad transition window directly around <code class="language-plaintext highlighter-rouge">04920</code>, rather than chasing residual state after the failure has already stabilized</li>
    </ul>
  </li>
</ul>

<h2 id="2026-03-28-1828-pdt">2026-03-28 18:28 PDT</h2>

<ul>
  <li>Tightened the first stable failure onset again with a 10-frame boundary scan:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-boundary5000i10/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>first stable bad frame is now pinned to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code></li>
        </ul>
      </li>
      <li>last frame before it:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Useful observation:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900 -&gt; frame_04910</code> is the real settled flip</li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910 -&gt; frame_04920</code> is byte-identical</li>
      <li>so <code class="language-plaintext highlighter-rouge">04910</code> is the first stable wrong-ocean frame, not just part of a slow drift</li>
    </ul>
  </li>
  <li>Behavioral proof:
    <ul>
      <li>temporarily bypassed <code class="language-plaintext highlighter-rouge">adsPlayIntro()</code> in <code class="language-plaintext highlighter-rouge">repo:/story.c</code> only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
      <li>validation run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-nointro/result.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the stable late bad ocean disappears entirely</li>
      <li>replaced by a stable black-screen path</li>
      <li>final state hash changes from the ocean hash to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">df57118101f4e25e7e8a74ed77aaadb00f9ed9ea28e1cff7236c070d1ffaed5e</code></li>
        </ul>
      </li>
      <li>visual detect at <code class="language-plaintext highlighter-rouge">frame_04910</code> and <code class="language-plaintext highlighter-rouge">frame_05000</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">black</code> / no island / no content</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled bad ocean is directly sourced by the normal <code class="language-plaintext highlighter-rouge">adsPlayIntro()</code> boot path</li>
      <li>bypassing intro does not fix ACTIVITY, but it proves the late ocean surface is not some unrelated later fallback</li>
      <li>it comes from the intro/ocean boot sequence that the story path normally uses before ACTIVITY should take over</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsPlayIntro()</code> bypass after validating the black-screen replacement path</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the active bug is now strongly centered on the handoff from intro/ocean boot into the ACTIVITY scene path</li>
      <li>not on the residual late pending override</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument the intro-to-scene handoff itself, because that is now the most plausible place where ACTIVITY takeover fails and leaves the boot ocean in place</li>
    </ul>
  </li>
</ul>

<h2 id="1832-pdt---post-intro-scrub-proof-changes-failure-to-stable-title-not-black-or-ocean">18:32 PDT - Post-intro scrub proof changes failure to stable title, not black or ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>left normal <code class="language-plaintext highlighter-rouge">adsPlayIntro()</code> in <code class="language-plaintext highlighter-rouge">repo:/story.c</code></li>
      <li>then immediately called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-postintroscrub/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
      <li>sampled late frames remain on the title surface, not ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770</code>: title</li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code>: title</li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code>: title</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important comparison:
    <ul>
      <li>normal path:
        <ul>
          <li>stable bad ocean by <code class="language-plaintext highlighter-rouge">04910</code></li>
        </ul>
      </li>
      <li>no-intro proof:
        <ul>
          <li>stable black-screen path</li>
        </ul>
      </li>
      <li>post-intro scrub proof:
        <ul>
          <li>stable title path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the bad ocean is not explained only by <code class="language-plaintext highlighter-rouge">adsPlayIntro()</code> being called</li>
      <li>it depends on intro-side island state surviving after intro returns</li>
      <li>clearing island state immediately after intro prevents the bad ocean and instead leaves the run stranded on title</li>
      <li>so the ACTIVITY failure is now most strongly centered on the handoff immediately after intro returns and before ACTIVITY content successfully takes over</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-intro <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument the immediate post-intro handoff, because that is now the narrowest boundary separating:
        <ul>
          <li>stable title</li>
          <li>stable wrong ocean</li>
          <li>black-screen no-intro failure</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1834-pdt---replacing-first-exact-activity-prepare-with-adsnoisland-yields-black-not-ocean">18:34 PDT - Replacing first exact ACTIVITY prepare with <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> yields black, not ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, replaced the first exact:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li><code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
          <li><code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
      <li>with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prepare-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 0f9a705ed4b557e621649636842b051f9311c39535f4fd6159c7b3243c98a54a</code></li>
        </ul>
      </li>
      <li>sampled late frames are black, not ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770</code>: black</li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code>: black</li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code>: black</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important comparison:
    <ul>
      <li>normal path:
        <ul>
          <li>stable bad ocean by <code class="language-plaintext highlighter-rouge">04910</code></li>
        </ul>
      </li>
      <li>no-intro proof:
        <ul>
          <li>stable black-screen path</li>
        </ul>
      </li>
      <li>post-intro scrub proof:
        <ul>
          <li>stable title path</li>
        </ul>
      </li>
      <li>exact-prepare <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof:
        <ul>
          <li>stable black-screen path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled bad ocean is being established by the exact first <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> prepare path, not merely carried forward from intro</li>
      <li>intro alone is not enough to explain the stable ocean:
        <ul>
          <li>keeping intro but scrubbing immediately after it gives title</li>
          <li>keeping intro but replacing the exact ACTIVITY prepare with <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> gives black</li>
        </ul>
      </li>
      <li>so the active failure boundary is now centered tightly on what <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code> does for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary exact-prepare <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> prepare path itself:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(finalScene)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsPrimeSceneResources(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
        </ul>
      </li>
      <li>because that path now has the strongest causal link to the stable wrong-ocean state</li>
    </ul>
  </li>
</ul>

<h2 id="1840-pdt---skipping-adsprimesceneresources-does-not-change-the-stable-ocean-failure">18:40 PDT - Skipping <code class="language-plaintext highlighter-rouge">adsPrimeSceneResources()</code> does not change the stable ocean failure</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, skipped:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPrimeSceneResources(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
      <li>left:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(scene)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>normal intro flow</li>
          <li>normal later playback path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-skip-prime/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = fe8a54dc926a6b88be9364720305f44a71ea7d5f511a15ab3c3462defac3996b</code></li>
        </ul>
      </li>
      <li>sampled late frames are still stable ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770</code>: ocean</li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code>: ocean</li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code>: ocean</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">adsPrimeSceneResources()</code> is not the primary cause of the settled wrong-ocean state</li>
      <li>removing it does not collapse the run to black and does not prevent the ocean failure</li>
      <li>so the strongest remaining prepare-path suspects are now:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(finalScene)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>specifically <code class="language-plaintext highlighter-rouge">islandInit()</code> and the initial <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code> path it performs</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary skip-prime proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>split the remaining exact prepare surface between:
        <ul>
          <li>island-state calculation</li>
          <li>and <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> / <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910">18:45 PDT - Skipping exact scene island-state calculation perturbs the lead-in, but ocean still settles by <code class="language-plaintext highlighter-rouge">04910</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, skipped:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(scene)</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
      <li>left:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPrimeSceneResources(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>normal intro flow</li>
          <li>normal later playback path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-skip-scene-islandcalc/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
      <li>sampled frames:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770</code>: black</li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code>: ocean</li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code>: ocean</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>scene-specific island-state calculation does affect the lead-in before the settled failure</li>
      <li>but it does not prevent the stable wrong-ocean state from being re-established by <code class="language-plaintext highlighter-rouge">04910</code></li>
      <li>so it is not sufficient to explain the persistent ACTIVITY failure on its own</li>
      <li>the strongest remaining causal surface is still:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>especially <code class="language-plaintext highlighter-rouge">islandInit()</code> and its initial background load/composition path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary skip-scene-islandcalc proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>isolate <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> / <code class="language-plaintext highlighter-rouge">islandInit()</code> more directly, because that is now the tightest surviving source of the settled wrong-ocean outcome</li>
    </ul>
  </li>
</ul>

<h2 id="0216-pdt---keeping-the-first-small-pack-header-buffer-alive-did-not-move-activity-1">02:16 PDT - Keeping the first small pack-header buffer alive did not move <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so the first small <code class="language-plaintext highlighter-rouge">PS1_PACK_HEADER_SIZE</code> allocation stayed alive until the function returned</li>
      <li>specifically:
        <ul>
          <li>kept the first <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(&amp;cdfile, 0, PS1_PACK_HEADER_SIZE)</code> buffer allocated</li>
          <li>delayed its <code class="language-plaintext highlighter-rouge">free(...)</code> until after the later full header read / entry decode completed</li>
        </ul>
      </li>
      <li>goal:
        <ul>
          <li>test whether immediate reuse of that first tiny heap block was the layout-sensitive seam</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-keep-small-header</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly with the exact old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>keeping the first small header allocation alive is not sufficient to move the bug</li>
      <li>so the layout/lifetime sensitivity around <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> is still real, but it is not explained by simple reuse of that first <code class="language-plaintext highlighter-rouge">20</code>-byte header block alone</li>
      <li>the stronger remaining suspects stay local to:
        <ul>
          <li>stack adjacency / local object layout</li>
          <li><code class="language-plaintext highlighter-rouge">CdlFILE</code> lifetime or overwrite</li>
          <li>interaction between the two <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> calls beyond just freeing the first tiny buffer</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">headerDataSmall</code> lifetime probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect local-object lifetime and overwrite risk around:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">cdfile</code></li>
          <li><code class="language-plaintext highlighter-rouge">headerData</code></li>
          <li><code class="language-plaintext highlighter-rouge">entries</code></li>
          <li>the transition between the first and second <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> calls</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0218-pdt---copying-cdlfile-immediately-after-cdsearchfile-moved-activity-1-to-the-third-branch">02:18 PDT - Copying <code class="language-plaintext highlighter-rouge">CdlFILE</code> immediately after <code class="language-plaintext highlighter-rouge">CdSearchFile</code> moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> to the third branch</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so that:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">stableCdfile = cdfile;</code></li>
          <li>both <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> calls used <code class="language-plaintext highlighter-rouge">stableCdfile</code></li>
          <li><code class="language-plaintext highlighter-rouge">outPack-&gt;cdfile</code> also stored <code class="language-plaintext highlighter-rouge">stableCdfile</code></li>
        </ul>
      </li>
      <li>no search logic changed</li>
      <li>goal:
        <ul>
          <li>test whether copying <code class="language-plaintext highlighter-rouge">CdlFILE</code> immediately after <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> is enough to perturb the layout-sensitive failure</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-stable-cdfile</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly with the third stable branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
      <li>late markers:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is a strong confirmation that the live seam is no longer behaving like a pure <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> logic bug</li>
      <li>copying <code class="language-plaintext highlighter-rouge">CdlFILE</code> immediately after search is enough to move the runtime, matching the earlier stack-layout branch</li>
      <li>the strongest active suspects are now:
        <ul>
          <li>local <code class="language-plaintext highlighter-rouge">CdlFILE</code> lifetime / overwrite</li>
          <li>adjacent stack layout around <code class="language-plaintext highlighter-rouge">cdfile</code>, <code class="language-plaintext highlighter-rouge">headerData</code>, and <code class="language-plaintext highlighter-rouge">entries</code></li>
          <li>UB or clobber between the first and second <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> calls</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">stableCdfile</code> probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>probe memory adjacency around <code class="language-plaintext highlighter-rouge">cdfile</code> more directly, rather than pack-search logic</li>
      <li>likely either:
        <ul>
          <li>duplicate <code class="language-plaintext highlighter-rouge">cdfile</code> into a far-separated local block</li>
          <li>or harden / relocate other nearby locals in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0221-pdt---moving-cdlfile-fully-off-the-local-stack-also-moved-activity-1">02:21 PDT - Moving <code class="language-plaintext highlighter-rouge">CdlFILE</code> fully off the local stack also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so the searched pack file record lived in a function-static <code class="language-plaintext highlighter-rouge">CdlFILE</code></li>
      <li>specifically:
        <ul>
          <li>removed local-stack <code class="language-plaintext highlighter-rouge">CdlFILE cdfile</code></li>
          <li>used <code class="language-plaintext highlighter-rouge">static CdlFILE stableCdfile</code></li>
          <li>passed <code class="language-plaintext highlighter-rouge">stableCdfile</code> to both <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> calls</li>
          <li>stored <code class="language-plaintext highlighter-rouge">stableCdfile</code> into <code class="language-plaintext highlighter-rouge">outPack-&gt;cdfile</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-static-cdfile</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
      <li>late markers:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 63</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is stronger than the previous local-copy probe</li>
      <li>moving <code class="language-plaintext highlighter-rouge">CdlFILE</code> completely off the hot local stack is enough to change the runtime</li>
      <li>together, the two <code class="language-plaintext highlighter-rouge">CdlFILE</code> probes now make the most likely live seam:
        <ul>
          <li>local-stack adjacency around <code class="language-plaintext highlighter-rouge">CdlFILE cdfile</code></li>
          <li>or a nearby overwrite / UB that depends on where <code class="language-plaintext highlighter-rouge">cdfile</code> is laid out relative to:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">headerData</code></li>
              <li><code class="language-plaintext highlighter-rouge">packFile</code></li>
              <li><code class="language-plaintext highlighter-rouge">cdPath</code></li>
              <li>later <code class="language-plaintext highlighter-rouge">packCdFile</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary function-static <code class="language-plaintext highlighter-rouge">CdlFILE</code> probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>probe which nearby local is interacting with <code class="language-plaintext highlighter-rouge">cdfile</code> layout</li>
      <li>likely by moving or padding one neighboring object at a time inside <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
</ul>

<h2 id="0224-pdt---moving-cdpath-off-the-local-stack-also-moved-activity-1">02:24 PDT - Moving <code class="language-plaintext highlighter-rouge">cdPath</code> off the local stack also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so the constructed pack search path lived in a function-static buffer</li>
      <li>specifically:
        <ul>
          <li>removed local-stack <code class="language-plaintext highlighter-rouge">char cdPath[64]</code></li>
          <li>used <code class="language-plaintext highlighter-rouge">static char stableCdPath[64]</code></li>
          <li>still kept local-stack <code class="language-plaintext highlighter-rouge">CdlFILE cdfile</code></li>
          <li>no search logic changed beyond where the path buffer lived</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-static-cdpath</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly on the third branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
      <li>late markers:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is the strongest adjacent-local clue so far</li>
      <li>moving only <code class="language-plaintext highlighter-rouge">cdPath</code> off the local stack is enough to perturb the bug, even while <code class="language-plaintext highlighter-rouge">cdfile</code> remains local</li>
      <li>that makes the hottest current seam the local-stack relationship between:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">char cdPath[64]</code></li>
          <li><code class="language-plaintext highlighter-rouge">CdlFILE cdfile</code></li>
        </ul>
      </li>
      <li>current best read:
        <ul>
          <li>either <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> or a nearby later operation is clobbering one of those objects</li>
          <li>or the bug depends on exact stack adjacency between them</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary function-static <code class="language-plaintext highlighter-rouge">cdPath</code> probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>probe the <code class="language-plaintext highlighter-rouge">cdPath</code> / <code class="language-plaintext highlighter-rouge">cdfile</code> seam directly</li>
      <li>likely with local padding between them, or by reordering just those two locals inside <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
</ul>

<h2 id="0227-pdt---simple-local-padding-between-cdpath-and-cdfile-did-not-move-activity-1">02:27 PDT - Simple local padding between <code class="language-plaintext highlighter-rouge">cdPath</code> and <code class="language-plaintext highlighter-rouge">cdfile</code> did not move <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so both:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">char cdPath[64]</code></li>
          <li><code class="language-plaintext highlighter-rouge">CdlFILE cdfile</code></li>
        </ul>
      </li>
      <li>remained local-stack objects</li>
      <li>but a local:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">volatile uint8_t cdPathPad[64]</code></li>
        </ul>
      </li>
      <li>was inserted between them and zeroed before <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-cdpathpad64</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly on the original old-ocean branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the live seam is more specific than raw stack distance between <code class="language-plaintext highlighter-rouge">cdPath</code> and <code class="language-plaintext highlighter-rouge">cdfile</code></li>
      <li>simply inserting padding is not enough</li>
      <li>the earlier moving probes likely depend on:
        <ul>
          <li>exact object identity / storage class</li>
          <li>or exact relative ordering/layout, not just added bytes in between</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">cdPathPad</code> probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>test exact local reordering rather than padding</li>
      <li>especially swapping or separating <code class="language-plaintext highlighter-rouge">cdPath</code> and <code class="language-plaintext highlighter-rouge">cdfile</code> by object order inside <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
</ul>

<h2 id="0231-pdt---swapping-local-order-of-cdfile-and-cdpath-moved-activity-1">02:31 PDT - Swapping local order of <code class="language-plaintext highlighter-rouge">cdfile</code> and <code class="language-plaintext highlighter-rouge">cdPath</code> moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> so only the declaration order changed:
        <ul>
          <li>from:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">char cdPath[64];</code></li>
              <li><code class="language-plaintext highlighter-rouge">CdlFILE cdfile;</code></li>
            </ul>
          </li>
          <li>to:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdlFILE cdfile;</code></li>
              <li><code class="language-plaintext highlighter-rouge">char cdPath[64];</code></li>
            </ul>
          </li>
        </ul>
      </li>
      <li>no logic changed</li>
      <li>no storage class changed</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-swap-cdfile-cdpath</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
      <li>late markers:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 63</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>exact local order matters here</li>
      <li>that is stronger than the earlier padding-negative result</li>
      <li>current best read:
        <ul>
          <li>the live seam is a layout-sensitive overwrite / UB involving the local-stack placement of:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">cdPath</code></li>
              <li><code class="language-plaintext highlighter-rouge">cdfile</code></li>
            </ul>
          </li>
          <li>not just raw adjacency distance</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary local-order swap in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>identify which operation actually clobbers or depends on that order</li>
      <li>likely by isolating:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code></li>
          <li>the first <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(&amp;cdfile, ...)</code></li>
          <li>and any later reuse of <code class="language-plaintext highlighter-rouge">cdfile</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0236-pdt---a-tiny-helper-around-cdsearchfile-also-moved-activity-1">02:36 PDT - A tiny helper around <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> also moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code>, added a tiny helper:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1PilotSearchFile(const char *path, CdlFILE *outFile)</code></li>
        </ul>
      </li>
      <li>and changed <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> to call that helper instead of invoking:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code></li>
          <li>directly</li>
        </ul>
      </li>
      <li>no logic changed</li>
      <li>no storage class changed</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-search-helper</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exited cleanly on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
      <li>late markers:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 63</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>changing the caller frame around <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> is enough to move the runtime</li>
      <li>that is the strongest current sign that the live seam is stack-sensitive corruption or UB at/around the <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code> call site itself</li>
      <li>current best read:
        <ul>
          <li>not a clean logical pack-search bug</li>
          <li>not just later pack-header parsing</li>
          <li>most likely a stack-sensitive overwrite interacting with:
            <ul>
              <li>local <code class="language-plaintext highlighter-rouge">cdPath</code></li>
              <li>local <code class="language-plaintext highlighter-rouge">cdfile</code></li>
              <li>and the exact call frame for <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">ps1PilotSearchFile(...)</code> helper probe in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>isolate the fault surface to:
        <ul>
          <li>the direct <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> call itself</li>
          <li>versus the first subsequent <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code></li>
        </ul>
      </li>
      <li>using equally small caller-frame perturbations</li>
    </ul>
  </li>
</ul>

<h2 id="0155-pdt---evaluating-the-old-activitypak-conditional-alone-is-enough-to-hold-the-improved-branch">01:55 PDT - Evaluating the old <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> conditional alone is enough to hold the improved branch</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>kept the same conditional shape that previously correlated with the improved branch:
            <ul>
              <li>literal <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;verifyFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
              <li>exact expected metadata checks</li>
              <li>second literal <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
              <li>mismatch test against the same expected metadata</li>
            </ul>
          </li>
          <li>but removed the early-return side effect</li>
          <li>only assigned the boolean result to a <code class="language-plaintext highlighter-rouge">volatile int proof_condition</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-cond-noop</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run stayed on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earlier improvement did not depend on the early return being taken</li>
      <li>evaluating that exact conditional is enough to perturb the failure</li>
      <li>so the active seam now looks like a timing / register / stack-layout sensitivity around the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup path, not a clean logical <code class="language-plaintext highlighter-rouge">CdSearchFile</code> branch bug</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary conditional-noop proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop treating this as a pure logic bug in the pack-lookup branch</li>
      <li>inspect whether tiny local-layout changes around <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code> are masking an adjacent memory clobber or <code class="language-plaintext highlighter-rouge">CdlFILE</code> lifetime issue</li>
    </ul>
  </li>
</ul>

<h2 id="0200-pdt---pure-local-stack-padding-moves-the-run-to-a-third-stable-branch">02:00 PDT - Pure local stack padding moves the run to a third stable branch</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, added:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">volatile uint8_t layoutPad[64];</code></li>
          <li><code class="language-plaintext highlighter-rouge">memset((void *)layoutPad, 0, sizeof(layoutPad));</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code></li>
      <li>no <code class="language-plaintext highlighter-rouge">CdSearchFile</code> logic was changed</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-layoutpad64</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run moved to a third stable branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
      <li>markers also changed:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_fail = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is the strongest evidence so far that the live seam is a local-memory / UB / layout-sensitive bug around <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
      <li>a pure stack-layout perturbation, with no lookup-logic change, is enough to move the runtime onto a different stable failure branch</li>
      <li>current best read:
        <ul>
          <li>adjacent local clobber</li>
          <li>or lifetime/overwrite involving <code class="language-plaintext highlighter-rouge">CdlFILE</code>, header buffers, or nearby stack locals</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary stack-padding proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect exact local-object lifetime and overwrite risk inside <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
      <li>especially:
        <ul>
          <li>stack-local adjacency</li>
          <li><code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> consumers</li>
          <li>and whether header/entry decode is reading/writing beyond intended bounds</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0205-pdt---basic-decoded-field-bounds-hardening-does-not-move-the-run">02:05 PDT - Basic decoded-field bounds hardening does not move the run</h2>

<ul>
  <li>Temporary hardening:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, added guards:
        <ul>
          <li>reject <code class="language-plaintext highlighter-rouge">entryBytes</code> multiply overflow</li>
          <li>reject <code class="language-plaintext highlighter-rouge">prefetchCount &gt; PS1_PACK_PREFETCH_MAX</code></li>
          <li>reject <code class="language-plaintext highlighter-rouge">firstResourceOffset &gt; cdfile.size</code></li>
        </ul>
      </li>
      <li>left all actual lookup and decode logic otherwise unchanged</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-bounds-guard1</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still fell back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the active bug is still not explained by the most obvious corrupted-header invariants</li>
      <li>current evidence still points more strongly at:
        <ul>
          <li>layout-sensitive UB</li>
          <li>local-memory clobber</li>
          <li>or object-lifetime misuse around <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary bounds guards</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>probe object adjacency/lifetime more directly:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">headerData</code></li>
          <li><code class="language-plaintext highlighter-rouge">entries</code></li>
          <li><code class="language-plaintext highlighter-rouge">cdfile</code></li>
          <li>and the transition between the first and second <code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code> results</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0124-pdt---root-file-lookup-is-healthy-before-activitypak-search-but-not-after-it">01:24 PDT - Root-file lookup is healthy before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search, but not after it</h2>

<ul>
  <li>Temporary proofs:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>before <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code>, checked:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;verifyFile, "\\RESOURCE.MAP;1")</code></li>
              <li>expected:
                <ul>
                  <li>size <code class="language-plaintext highlighter-rouge">1461</code></li>
                  <li>extent <code class="language-plaintext highlighter-rouge">416</code></li>
                </ul>
              </li>
            </ul>
          </li>
          <li>and temporarily returned <code class="language-plaintext highlighter-rouge">0</code> if that lookup matched exactly</li>
        </ul>
      </li>
      <li>separate run:
        <ul>
          <li>after the normal <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code>, checked the same <code class="language-plaintext highlighter-rouge">\\RESOURCE.MAP;1</code> metadata and returned <code class="language-plaintext highlighter-rouge">0</code> only if it still matched</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-rootmap-presearch-proof</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-rootmap-control-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>presearch proof snapped back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li>postsearch proof stayed on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">CdSearchFile</code> is healthy on entry to <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
      <li>but after the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup, even a known-good root file no longer resolves with the expected metadata</li>
      <li>so the <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search itself is poisoning later <code class="language-plaintext highlighter-rouge">CdSearchFile</code> state</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">RESOURCE.MAP</code> proofs</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>determine whether pack-directory lookup is also healthy before the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search</li>
    </ul>
  </li>
</ul>

<h2 id="0131-pdt---another-pack-file-resolves-correctly-before-activitypak-search">01:31 PDT - Another pack file resolves correctly before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>before the normal <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> search, checked:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;verifyFile, "\\PACKS\\BUILDING.PAK;1")</code></li>
              <li>expected:
                <ul>
                  <li>size <code class="language-plaintext highlighter-rouge">2441216</code></li>
                  <li>extent <code class="language-plaintext highlighter-rouge">5037</code></li>
                </ul>
              </li>
            </ul>
          </li>
          <li>and temporarily returned <code class="language-plaintext highlighter-rouge">0</code> if that lookup matched exactly</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-building-presearch-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run snapped back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>pack-directory lookup is also healthy on entry</li>
      <li>so the failure is not a general inability to resolve <code class="language-plaintext highlighter-rouge">PACKS\\*.PAK;1</code></li>
      <li>the live seam is now narrower: the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup itself</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">BUILDING.PAK</code> presearch proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>rule out any remaining path-string construction issue by testing a literal <code class="language-plaintext highlighter-rouge">\\PACKS\\ACTIVITY.PAK;1</code> lookup</li>
    </ul>
  </li>
</ul>

<h2 id="0132-pdt---literal-packsactivitypak1-lookup-fails-too">01:32 PDT - Literal <code class="language-plaintext highlighter-rouge">\\PACKS\\ACTIVITY.PAK;1</code> lookup fails too</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>before the normal <code class="language-plaintext highlighter-rouge">cdPath</code> search, checked a literal:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;verifyFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
              <li>expected:
                <ul>
                  <li>size <code class="language-plaintext highlighter-rouge">2836480</code></li>
                  <li>extent <code class="language-plaintext highlighter-rouge">3652</code></li>
                </ul>
              </li>
            </ul>
          </li>
          <li>and temporarily returned <code class="language-plaintext highlighter-rouge">0</code> if that literal lookup matched exactly</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-literal-presearch-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run stayed on the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not a <code class="language-plaintext highlighter-rouge">cdPath</code> construction or <code class="language-plaintext highlighter-rouge">ps1PilotBuildPackFile(...)</code> string-format issue</li>
      <li>on current <code class="language-plaintext highlighter-rouge">HEAD</code>, exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> resolution is already wrong even when searched via the literal correct path, while:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">\\RESOURCE.MAP;1</code> resolves correctly before it</li>
          <li><code class="language-plaintext highlighter-rouge">\\PACKS\\BUILDING.PAK;1</code> resolves correctly before it</li>
        </ul>
      </li>
      <li>the active root-cause surface is now the exact <code class="language-plaintext highlighter-rouge">CdSearchFile("\\PACKS\\ACTIVITY.PAK;1")</code> lookup behavior itself</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary literal-lookup proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect why exact <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup is special:
        <ul>
          <li>possible duplicate/conflicting directory record</li>
          <li>path aliasing</li>
          <li>or pack-name-specific disc lookup behavior under DuckStation/PSn00b CD APIs</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0135-pdt---the-image-has-exactly-one-activitypak-record-and-repeated-literal-lookups-stay-on-the-improved-branch">01:35 PDT - The image has exactly one <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> record, and repeated literal lookups stay on the improved branch</h2>

<ul>
  <li>Built-image inspection:
    <ul>
      <li>parsed <code class="language-plaintext highlighter-rouge">repo:/jcreborn.bin</code> directly</li>
      <li><code class="language-plaintext highlighter-rouge">PACKS</code> directory listing is clean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY.PAK;1</code> extent <code class="language-plaintext highlighter-rouge">3652</code>, size <code class="language-plaintext highlighter-rouge">2836480</code></li>
          <li><code class="language-plaintext highlighter-rouge">BUILDING.PAK;1</code> extent <code class="language-plaintext highlighter-rouge">5037</code>, size <code class="language-plaintext highlighter-rouge">2441216</code></li>
          <li>no duplicate <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK;1</code> directory records</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>performed a first literal:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;verifyFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>required it to match:
            <ul>
              <li>size <code class="language-plaintext highlighter-rouge">2836480</code></li>
              <li>extent <code class="language-plaintext highlighter-rouge">3652</code></li>
            </ul>
          </li>
          <li>then immediately performed a second identical literal:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>and would temporarily return <code class="language-plaintext highlighter-rouge">0</code> only if that second lookup came back wrong</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-doublelookup-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run stayed on the improved branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the disc image itself is not the issue:
        <ul>
          <li>there is a single clean <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK;1</code> record</li>
        </ul>
      </li>
      <li>two consecutive literal <code class="language-plaintext highlighter-rouge">CdSearchFile("\\PACKS\\ACTIVITY.PAK;1")</code> calls do not reproduce the old failure</li>
      <li>so the live seam is now narrower than “ACTIVITY record is bad”</li>
      <li>current best read:
        <ul>
          <li>the normal runtime path can be destabilized by how it reaches the later <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code> call</li>
          <li>but a literal ACTIVITY lookup can also act as a stabilizing/priming step before the normal path continues</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary repeated-literal proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>test whether a single literal <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> prelookup, with no mismatch check and no early return, is enough to stabilize the normal runtime path</li>
    </ul>
  </li>
</ul>

<h2 id="0140-pdt---a-single-literal-activitypak-prelookup-is-not-enough">01:40 PDT - A single literal <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> prelookup is not enough</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>inserted a no-op prelookup:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;primeFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>then continued into the normal:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code></li>
            </ul>
          </li>
          <li>no early return, no mismatch branch</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-prime-only</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run fell back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earlier repeated-literal stabilization effect is real, but it is not explained by simple one-shot priming alone</li>
      <li>the live seam is now narrower:
        <ul>
          <li>something about the exact repeated lookup/use pattern matters</li>
          <li>not just “search ACTIVITY once before the normal path”</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary prime-only prelookup</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>compare the successful double-literal pattern against the failing prime-only pattern more directly</li>
      <li>likely focus on:
        <ul>
          <li>which <code class="language-plaintext highlighter-rouge">CdlFILE</code> instance is later reused</li>
          <li>and whether avoiding the final <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code> entirely changes the path</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0145-pdt---reusing-a-known-good-literal-cdlfile-still-does-not-stabilize-the-normal-path">01:45 PDT - Reusing a known-good literal <code class="language-plaintext highlighter-rouge">CdlFILE</code> still does not stabilize the normal path</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>performed:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;primeFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>then skipped the normal <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, cdPath)</code> by copying:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">cdfile = primeFile</code></li>
            </ul>
          </li>
          <li>the rest of the pack-index path was unchanged</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-reuse-primefile</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still fell back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earlier repeated-literal stabilization effect is narrower than:
        <ul>
          <li>one-shot priming</li>
          <li>or reusing a known-good <code class="language-plaintext highlighter-rouge">CdlFILE</code> directly</li>
        </ul>
      </li>
      <li>so the remaining seam is likely in the exact lookup/use sequence of the successful double-literal proof, not just in the final normal <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> call or the <code class="language-plaintext highlighter-rouge">CdlFILE</code> value alone</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">cdfile = primeFile</code> proof</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>compare the exact successful double-literal proof flow against the failing reuse flow</li>
      <li>likely inspect whether the success depended on:
        <ul>
          <li>performing the second literal <code class="language-plaintext highlighter-rouge">CdSearchFile(...)</code> into the same destination variable later used by the rest of the function</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0150-pdt---double-literal-lookup-without-the-old-conditional-still-falls-back">01:50 PDT - Double literal lookup without the old conditional still falls back</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>performed:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;primeFile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>then continued normally</li>
          <li>no early return, no mismatch check</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activitypak-doubleliteral-noearly</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still fell back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earlier “successful” double-literal result was not a practical stabilizer sequence by itself</li>
      <li>it depended on the exact conditional proof structure around it, not just on executing two literal lookups</li>
      <li>that pushes the live seam back toward:
        <ul>
          <li>subtle branch/control-flow perturbation from the proof itself</li>
          <li>rather than a reusable <code class="language-plaintext highlighter-rouge">CdSearchFile</code> workaround</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary double-literal-noearly probe</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>compare the original successful conditional proof shape against the clean negative variants</li>
      <li>specifically isolate whether the old success came from:
        <ul>
          <li>skipping later code via the conditional structure</li>
          <li>or changing register/stack/timing enough to mask the underlying fault</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0036-pdt---activity-ads-metadata-is-already-wrong-immediately-after-ps1_loadadsdata">00:36 PDT - ACTIVITY ADS metadata is already wrong immediately after <code class="language-plaintext highlighter-rouge">ps1_loadAdsData()</code></h2>

<ul>
  <li>Active scene/debug base:
    <ul>
      <li>current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
      <li>exact scene:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Strong parser-side results established this session:
    <ul>
      <li>exact ACTIVITY entry offset is still correct after <code class="language-plaintext highlighter-rouge">adsLoad(...)</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">offset == 2</code></li>
        </ul>
      </li>
      <li>but these invariants do not hold on the PS1 runtime path:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNumTags == 10</code></li>
          <li><code class="language-plaintext highlighter-rouge">numAdsChunks == 6</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsFindTag(12) == 176</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>New earlier proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, immediately after the PS1 lazy ADS load block and before:
        <ul>
          <li>pilot priming</li>
          <li><code class="language-plaintext highlighter-rouge">pinResource(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">checkMemoryBudget()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, added a guard:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">adsResource-&gt;uncompressedSize != 2558</code></li>
          <li>call:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads-size-early-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04920</code> stayed on the improved black lead-in:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>late frames changed to the same alternate persistent state seen in the earlier byte-proof:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the ACTIVITY ADS corruption is earlier than:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">pinResource(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">checkMemoryBudget()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
        </ul>
      </li>
      <li>by the time <code class="language-plaintext highlighter-rouge">ps1_loadAdsData()</code> returns, exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> already has the wrong <code class="language-plaintext highlighter-rouge">uncompressedSize</code></li>
      <li>that shifts the live root-cause surface into:
        <ul>
          <li>PS1 ADS load path</li>
          <li>or ADS resource metadata integrity before/inside <code class="language-plaintext highlighter-rouge">ps1_loadAdsData()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early-size proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1_loadAdsData(...)</code></li>
          <li>ADS metadata parse path in <code class="language-plaintext highlighter-rouge">resource.c</code></li>
          <li>any mismatch between authored ACTIVITY metadata (<code class="language-plaintext highlighter-rouge">uncompressedSize = 2558</code>) and runtime PS1 metadata/load behavior</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2355-pdt---older-6dae8410-base-is-not-a-trustworthy-post-boot-ps1-scene-debug-target">23:55 PDT - Older <code class="language-plaintext highlighter-rouge">6dae8410</code> base is not a trustworthy post-boot PS1 scene-debug target</h2>

<ul>
  <li>Active validation base:
    <ul>
      <li>detached worktree at <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae</code></li>
      <li>commit:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">6dae8410</code></li>
          <li><code class="language-plaintext highlighter-rouge">Fix PS1 first-handoff blackscreen</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>What I validated:
    <ul>
      <li>the old-base regtest harness really is staging the requested boot override into the snapshot disc:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">strings -a regtest-results/activity4-6dae-directads4/disc/jcreborn.bin</code></li>
          <li>contains:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code></li>
            </ul>
          </li>
        </ul>
      </li>
      <li>the old-base PS1 executable really is changing between proof runs:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">activity4-6dae-ps1load-entry-trap2</code>:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">Hash for 'JCREBORN.EXE' - 6A2E4F25CCD7CF0E</code></li>
            </ul>
          </li>
          <li><code class="language-plaintext highlighter-rouge">activity4-6dae-main-entry-trap</code>:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">Hash for 'JCREBORN.EXE' - 6880449D0B51BA34</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Strong proofs that failed to fire despite different executables:
    <ul>
      <li>temporary non-optimizable infinite-loop trap at the top of:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code></li>
        </ul>
      </li>
      <li>validation run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-ps1load-entry-trap2</code></li>
        </ul>
      </li>
      <li>result:
        <ul>
          <li>exited with success</li>
          <li><code class="language-plaintext highlighter-rouge">Frames dumped: 96</code></li>
          <li><code class="language-plaintext highlighter-rouge">Total execution time: 2464.34ms</code></li>
        </ul>
      </li>
      <li>temporary non-optimizable infinite-loop trap at the very top of:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">main()</code></li>
        </ul>
      </li>
      <li>validation run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-main-entry-trap</code></li>
        </ul>
      </li>
      <li>result:
        <ul>
          <li>exited with success</li>
          <li><code class="language-plaintext highlighter-rouge">Frames dumped: 96</code></li>
          <li><code class="language-plaintext highlighter-rouge">Total execution time: 3811.94ms</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>on <code class="language-plaintext highlighter-rouge">6dae8410</code>, the captured PS1 run is not behaving like a trustworthy post-<code class="language-plaintext highlighter-rouge">main()</code> game execution target</li>
      <li>the disc snapshot does contain the requested <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code> override</li>
      <li>but even hard traps at <code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code> entry and <code class="language-plaintext highlighter-rouge">main()</code> entry do not stall execution</li>
      <li>therefore this older base is not a defensible target for deeper scene-by-scene ADS debugging above pre-<code class="language-plaintext highlighter-rouge">main()</code> startup</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code> and <code class="language-plaintext highlighter-rouge">main()</code> trap probes after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop using <code class="language-plaintext highlighter-rouge">6dae8410</code> as the primary scene-debug base</li>
      <li>choose a newer PS1 snapshot that actually reaches normal runtime code paths, then continue one-scene-at-a-time from there</li>
    </ul>
  </li>
</ul>

<h2 id="0005-pdt---returned-to-current-head-activity-1-remains-the-live-scene-target">00:05 PDT - Returned to current <code class="language-plaintext highlighter-rouge">HEAD</code>; <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> remains the live scene target</h2>

<ul>
  <li>What I validated:
    <ul>
      <li>the real PS1-side <code class="language-plaintext highlighter-rouge">adsTags</code> lifetime fix is still present in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>:
        <ul>
          <li>PS1 path uses:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ps1StaticAdsTags</code></li>
            </ul>
          </li>
          <li>instead of per-play heap allocation</li>
        </ul>
      </li>
      <li>the earlier improved ACTIVITY result artifacts are still the right baseline:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-static-adstags-fix/result.json</code></li>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-static-adstags-fix-6300/result.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current ACTIVITY 1 read on <code class="language-plaintext highlighter-rouge">HEAD</code>:
    <ul>
      <li>the old settled bootstrap collapse is gone</li>
      <li>but the scene is still not correct</li>
      <li>by <code class="language-plaintext highlighter-rouge">6300</code> frames the run settles to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">screen_type = ocean</code></li>
          <li><code class="language-plaintext highlighter-rouge">island_present = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">johnny_present = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">launched = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">bmp_ok = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sprite_count_estimate = 63</code></li>
        </ul>
      </li>
      <li>strongest artifact:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-static-adstags-fix-6300/20260328-215318/frames/jcreborn/frame_06300.png</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Harness note:
    <ul>
      <li>a fresh compare-wrapper rerun for <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> on current <code class="language-plaintext highlighter-rouge">HEAD</code> completed the PS1 capture phase but hung in post-capture compare generation</li>
      <li>raw scene artifacts were produced under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/reference-compare-current-activity1/activity-1</code></li>
        </ul>
      </li>
      <li>but no final <code class="language-plaintext highlighter-rouge">compare.json</code> / <code class="language-plaintext highlighter-rouge">compare.html</code> was emitted before the wrapper was stopped</li>
      <li>that is a secondary harness issue, not the active scene root cause</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">6dae8410</code> is out</li>
      <li>current <code class="language-plaintext highlighter-rouge">HEAD</code> is back to being the only defensible live PS1 scene-debug base</li>
      <li>active scene remains:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
        </ul>
      </li>
      <li>active bug surface remains:
        <ul>
          <li>exact ACTIVITY bootstrap no longer fully collapses</li>
          <li>but later live playback still fails to take ownership of the scene, leaving a bad late ocean state</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>continue on <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> from current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
      <li>debug the post-bootstrap playback path after the static-<code class="language-plaintext highlighter-rouge">adsTags</code> fix, not the older pre-<code class="language-plaintext highlighter-rouge">main()</code> validation base</li>
    </ul>
  </li>
</ul>

<h2 id="0012-pdt---current-activity-1-still-crosses-the-old-pre-chunk-slot-1-corruption-seam">00:12 PDT - Current <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> still crosses the old pre-chunk slot-1 corruption seam</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, immediately after the exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> TTM load loop and before the first <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code>, added a temporary guard:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">ttmSlots[1].ttmResource</code> was not <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code></li>
          <li>call:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">unpinResource(adsResource, "ADS")</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-slot1-prechunk-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04920</code> stayed on the post-<code class="language-plaintext highlighter-rouge">adsTags</code> improved black lead-in:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>but late frames regressed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the static <code class="language-plaintext highlighter-rouge">adsTags</code> fix did move the bootstrap behavior</li>
      <li>but current <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> still crosses the earlier pre-chunk slot/resource corruption seam</li>
      <li>the strongest live suspect remains:
        <ul>
          <li>slot <code class="language-plaintext highlighter-rouge">1</code> resource integrity between:
            <ul>
              <li>the <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> loop</li>
              <li>and the first exact <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary slot-1 prechunk proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect what mutates or invalidates <code class="language-plaintext highlighter-rouge">ttmSlots[1]</code> in that narrow pre-chunk window on current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
    </ul>
  </li>
</ul>

<h2 id="0018-pdt---activity-1-slot-1-corruption-is-already-present-by-the-time-adsload-returns">00:18 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> slot-1 corruption is already present by the time <code class="language-plaintext highlighter-rouge">adsLoad()</code> returns</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(data, dataSize, adsResource-&gt;numTags, adsTag, &amp;offset);</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, added a temporary guard:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">ttmSlots[1].ttmResource</code> is not <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code></li>
          <li>call:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">unpinResource(adsResource, "ADS")</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-post-adsload-slot1-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04920</code> stayed on the improved black lead-in:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>late frames changed to a different persistent state:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: <code class="language-plaintext highlighter-rouge">81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this guard still perturbs the long-run ACTIVITY outcome</li>
      <li>therefore the slot/resource corruption seam is already crossed by the time <code class="language-plaintext highlighter-rouge">adsLoad()</code> returns</li>
      <li>that moves the active root-cause surface earlier than:
        <ul>
          <li>first exact <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code></li>
        </ul>
      </li>
      <li>strongest live suspect now:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code> itself, or state it mutates while parsing tags/chunks</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsLoad()</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> for side effects that can invalidate slot/resource state on current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
    </ul>
  </li>
</ul>

<h2 id="0025-pdt---adsload-runtime-tag-table-no-longer-preserves-authored-activity-startup-tag-12---176">00:25 PDT - <code class="language-plaintext highlighter-rouge">adsLoad()</code> runtime tag table no longer preserves authored ACTIVITY startup tag <code class="language-plaintext highlighter-rouge">12 -&gt; 176</code></h2>

<ul>
  <li>Offline authored fact:
    <ul>
      <li>extracted <code class="language-plaintext highlighter-rouge">repo:/jc_resources/extracted/ads/ACTIVITY.ADS</code> parses to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">10</code> tags total</li>
          <li>tag <code class="language-plaintext highlighter-rouge">1</code> offset <code class="language-plaintext highlighter-rouge">2</code></li>
          <li>first-chunk bookmarked globals <code class="language-plaintext highlighter-rouge">= 6</code></li>
          <li>tag <code class="language-plaintext highlighter-rouge">12</code> offset <code class="language-plaintext highlighter-rouge">176</code></li>
          <li>tag <code class="language-plaintext highlighter-rouge">11</code> offset <code class="language-plaintext highlighter-rouge">364</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, immediately after <code class="language-plaintext highlighter-rouge">adsLoad(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, added a guard:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">adsFindTag(12) != 176</code></li>
          <li>call:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
              <li><code class="language-plaintext highlighter-rouge">unpinResource(adsResource, "ADS")</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-tag12-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04920</code> stayed on the improved black lead-in:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>late frames regressed to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_06000</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06150</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06300</code>: <code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this guard clearly fired</li>
      <li>so after runtime <code class="language-plaintext highlighter-rouge">adsLoad()</code> on current <code class="language-plaintext highlighter-rouge">HEAD</code>, the exact ACTIVITY startup tag table is already wrong enough that:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsFindTag(12) != 176</code></li>
        </ul>
      </li>
      <li>this is stronger than the earlier bundled parse proof:
        <ul>
          <li>the corruption is not just “some parse invariant”</li>
          <li>it directly affects the authored startup tag lookup needed to bootstrap ACTIVITY</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsFindTag(12)</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>distinguish whether runtime <code class="language-plaintext highlighter-rouge">adsFindTag(12)</code> is:
        <ul>
          <li>missing entirely (<code class="language-plaintext highlighter-rouge">0</code>)</li>
          <li>or present but wrong offset</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0031-pdt---activityads-parse-time-metadata-is-still-correct-corruption-happens-later">00:31 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> parse-time metadata is still correct; corruption happens later</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, moved the exact-size guard earlier so it checked:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsResource-&gt;uncompressedSize != 2558</code></li>
        </ul>
      </li>
      <li>before calling <code class="language-plaintext highlighter-rouge">ps1_loadAdsData(...)</code></li>
      <li>then moved the same proof one step earlier again into <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1_parseAdsResource(...)</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> parsed with <code class="language-plaintext highlighter-rouge">uncompressedSize != 2558</code></li>
          <li>return <code class="language-plaintext highlighter-rouge">NULL</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-preload-metadata-proof</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-parse-size-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>pre-load metadata proof changed the run back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li>parse-time proof did not fire; run stayed on the improved post-<code class="language-plaintext highlighter-rouge">adsTags</code> branch:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> resource metadata parses correctly from the resource table</li>
      <li>the bad <code class="language-plaintext highlighter-rouge">uncompressedSize</code> appears later, after resource-table build but before first visible ACTIVITY playback</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary parse-time proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>split the lazy-load path:
        <ul>
          <li>pilot-pack ADS load</li>
          <li>raw-file fallback</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0039-pdt---the-bad-size-comes-from-the-pilot-pack-ads-load-path-not-from-authored-pack-content">00:39 PDT - The bad size comes from the pilot-pack ADS load path, not from authored pack content</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1_loadAdsData(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">ps1PilotLoadResource("ads", ...)</code> succeeds but returns <code class="language-plaintext highlighter-rouge">readSize != 2558</code></li>
          <li>drop the loaded pointer and return</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-pilotload-size-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the proof fired on the pilot-pack success path</li>
      <li>so the bad ACTIVITY size is being sourced by <code class="language-plaintext highlighter-rouge">ps1PilotLoadResource(...)</code></li>
      <li>it is not explained by parse-time metadata and does not require the raw-file fallback path</li>
    </ul>
  </li>
  <li>Additional validation:
    <ul>
      <li>the authored on-disk pack is clean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/jc_resources/packs/ACTIVITY.PAK</code></li>
        </ul>
      </li>
      <li>offline header parse shows:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> entry size <code class="language-plaintext highlighter-rouge">2558</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pilot-load proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>determine whether the in-memory pack index is already wrong when loaded, or only becomes wrong later in pack-entry lifetime</li>
    </ul>
  </li>
</ul>

<h2 id="0045-pdt---runtime-activitypak-index-handling-is-still-the-live-seam">00:45 PDT - Runtime <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> index handling is still the live seam</h2>

<ul>
  <li>Temporary proofs:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> pack index decodes an <code class="language-plaintext highlighter-rouge">ads/ACTIVITY.ADS</code> entry with <code class="language-plaintext highlighter-rouge">sizeBytes != 2558</code></li>
          <li>return <code class="language-plaintext highlighter-rouge">0</code></li>
        </ul>
      </li>
      <li>then tried two read-path variants for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> pack index load:
        <ul>
          <li>whole-file read for the larger second header/index read only</li>
          <li>whole-file read for both pack-header reads</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-size-proof</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packindex-wholeproof</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packheader-wholeproof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>all three cuts still land on the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the on-disk pack header is correct, but the runtime pack-index path for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> is still the live seam</li>
      <li>switching the larger index read, or both header reads, to the whole-file path did not recover the improved ACTIVITY branch</li>
      <li>so the remaining suspect space is:
        <ul>
          <li>corrupted bytes returned by the runtime pack-index read path</li>
          <li>or later corruption of the active pack entry table after a correct decode</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary whole-read pack-index proof patches after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect active-pack table lifetime and ownership after <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
      <li>specifically whether <code class="language-plaintext highlighter-rouge">ps1PilotActivePack.entries</code> is being overwritten after pack activation for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code></li>
    </ul>
  </li>
</ul>

<h2 id="0050-pdt---ps1pilotactivepackentries-is-already-wrong-immediately-after-pack-activation">00:50 PDT - <code class="language-plaintext highlighter-rouge">ps1PilotActivePack.entries</code> is already wrong immediately after pack activation</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1_loadAdsData(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>immediately after:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ps1PilotSetActivePackForAds(adsResource-&gt;resName);</code></li>
            </ul>
          </li>
          <li>looked up:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ps1PilotFindEntry("ads", adsResource-&gt;resName)</code></li>
            </ul>
          </li>
          <li>if the entry existed and <code class="language-plaintext highlighter-rouge">entry-&gt;sizeBytes != 2558</code>
            <ul>
              <li>returned immediately before <code class="language-plaintext highlighter-rouge">ps1PilotLoadResource(...)</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-activepack-entry-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the proof fired before <code class="language-plaintext highlighter-rouge">ps1PilotLoadResource(...)</code> even ran</li>
      <li>so the active-pack table is already wrong as soon as <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> pack activation completes</li>
      <li>the live seam is now earlier than ADS byte loading itself:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1PilotSetActivePackForAds(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code></li>
          <li>or stale/corrupted lifetime of <code class="language-plaintext highlighter-rouge">ps1PilotActivePack.entries</code> before this call returns</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary active-pack-entry proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect pack activation and active-pack cache lifetime directly</li>
      <li>specifically whether:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1PilotSetActivePackForAds(...)</code> is reusing a stale <code class="language-plaintext highlighter-rouge">ps1PilotActivePack</code></li>
          <li>or <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code> populates a bad in-memory table even though the on-disk pack is correct</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0055-pdt---cold-resetting-activeprefetch-packs-does-not-recover-activity-1">00:55 PDT - Cold-resetting active/prefetch packs does not recover <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1_loadAdsData(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>before <code class="language-plaintext highlighter-rouge">ps1PilotSetActivePackForAds(...)</code>, forcibly:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ps1PilotResetActivePack();</code></li>
              <li><code class="language-plaintext highlighter-rouge">ps1PilotResetPack(&amp;ps1PilotPrefetchPack);</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-coldpack-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>stale active/prefetch cache reuse is not sufficient to explain the bad ACTIVITY pack entry</li>
      <li>even a cold activation path still produces the wrong in-memory <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> table on current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
      <li>the remaining live seam is now tighter:
        <ul>
          <li>fresh <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex("ACTIVITY.ADS", ...)</code></li>
          <li>or the CD read bytes feeding that fresh decode</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary cold-pack proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument fresh <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code> more directly</li>
      <li>especially the decoded <code class="language-plaintext highlighter-rouge">entryCount</code> / <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> entry fields inside the newly allocated <code class="language-plaintext highlighter-rouge">entries[]</code> table before it is installed as the active pack</li>
    </ul>
  </li>
</ul>

<h2 id="0100-pdt---fresh-activitypak-header-parse-is-already-wrong-on-the-first-20-byte-read">01:00 PDT - Fresh <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header parse is already wrong on the first 20-byte read</h2>

<ul>
  <li>Authored on-disk constants:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header decodes to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">entryCount = 71</code></li>
          <li><code class="language-plaintext highlighter-rouge">firstResourceOffset = 2048</code></li>
          <li><code class="language-plaintext highlighter-rouge">prefetchCount = 1</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>after the first <code class="language-plaintext highlighter-rouge">PS1_PACK_HEADER_SIZE</code> read and field decode</li>
          <li>if any of:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">entryCount != 71</code></li>
              <li><code class="language-plaintext highlighter-rouge">firstResourceOffset != 2048</code></li>
              <li><code class="language-plaintext highlighter-rouge">prefetchCount != 1</code></li>
            </ul>
          </li>
          <li>return <code class="language-plaintext highlighter-rouge">0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packheader-fields-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this proof fired before any entry-table decode</li>
      <li>so the very first 20-byte <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header read is already wrong on the fresh runtime path</li>
      <li>that rules out:
        <ul>
          <li>stale pack reuse</li>
          <li>late active-pack entry corruption as the first cause</li>
          <li>entry-table decode as the earliest fault</li>
        </ul>
      </li>
      <li>the live seam is now the pack-header CD read itself:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(&amp;cdfile, 0, PS1_PACK_HEADER_SIZE)</code></li>
          <li>or the <code class="language-plaintext highlighter-rouge">CdSearchFile</code> / <code class="language-plaintext highlighter-rouge">cdfile</code> basis feeding it</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pack-header-fields proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>debug the <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header CD read path directly</li>
      <li>compare:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFile(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">ps1_streamReadFromCdFileWhole(...)</code></li>
          <li>and raw whole-file CD reads for the first 20-byte pack header only</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0105-pdt---raw-whole-file-cd-read-for-the-first-activitypak-header-still-fails-the-same-way">01:05 PDT - Raw whole-file CD read for the first <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> header still fails the same way</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>replaced the first <code class="language-plaintext highlighter-rouge">PS1_PACK_HEADER_SIZE</code> read with:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ps1_loadRawFile("\\PACKS\\ACTIVITY.PAK;1", &amp;rawPackSize)</code></li>
            </ul>
          </li>
          <li>copied only the first 20 bytes into <code class="language-plaintext highlighter-rouge">headerData</code></li>
          <li>then let normal header field decode continue</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packheader-rawproof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>switching the first pack-header read from the range-read helper to the raw whole-file CD path did not recover the improved ACTIVITY branch</li>
      <li>so the earliest fault is now above the simple choice of:
        <ul>
          <li>range read</li>
          <li>whole-file slice</li>
          <li>raw whole-file read</li>
        </ul>
      </li>
      <li>the live seam is now likely:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">CdSearchFile</code> / file-info basis for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code></li>
          <li>or broader CD state/path handling before the first header bytes are interpreted</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary raw-header proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the <code class="language-plaintext highlighter-rouge">CdlFILE</code> / path / file-size basis for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> at pack activation time</li>
      <li>specifically whether <code class="language-plaintext highlighter-rouge">CdSearchFile</code> is resolving the expected pack file metadata before any read occurs</li>
    </ul>
  </li>
</ul>

<h2 id="0110-pdt---cdsearchfile-metadata-for-activitypak-is-already-wrong-before-any-header-bytes-are-read">01:10 PDT - <code class="language-plaintext highlighter-rouge">CdSearchFile</code> metadata for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> is already wrong before any header bytes are read</h2>

<ul>
  <li>Authored on-disk fact:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/jc_resources/packs/ACTIVITY.PAK</code></li>
      <li>size:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">2836480</code> bytes</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>immediately after:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
          <li>if:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">cdfile.size != 2836480</code></li>
            </ul>
          </li>
          <li>return <code class="language-plaintext highlighter-rouge">0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-pack-cdfile-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this proof fired before any header bytes were read</li>
      <li>so the earliest fault now exposed is <code class="language-plaintext highlighter-rouge">CdSearchFile</code> metadata for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code></li>
      <li>that moves the live seam above:
        <ul>
          <li>pack-header reads</li>
          <li>entry-table decode</li>
          <li>active-pack table lifetime</li>
        </ul>
      </li>
      <li>strongest current suspect:
        <ul>
          <li>path/file-resolution or disc-image lookup state for <code class="language-plaintext highlighter-rouge">PACKS\\ACTIVITY.PAK;1</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">cdfile.size</code> proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect why <code class="language-plaintext highlighter-rouge">CdSearchFile</code> resolves the wrong <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> metadata on current <code class="language-plaintext highlighter-rouge">HEAD</code></li>
      <li>likely compare:
        <ul>
          <li>exact path string</li>
          <li>disc image contents</li>
          <li>and any conflicting duplicate files or naming/case issues in the PS1 image build</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0116-pdt---cdsearchfile-is-not-just-returning-the-wrong-size-the-resolved-activitypak-start-sector-is-wrong-too">01:16 PDT - <code class="language-plaintext highlighter-rouge">CdSearchFile</code> is not just returning the wrong size; the resolved <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> start sector is wrong too</h2>

<ul>
  <li>Built image fact:
    <ul>
      <li>parsed directly from <code class="language-plaintext highlighter-rouge">repo:/jcreborn.bin</code></li>
      <li>ISO entry:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">PACKS/ACTIVITY.PAK;1</code></li>
          <li>extent <code class="language-plaintext highlighter-rouge">3652</code></li>
          <li>size <code class="language-plaintext highlighter-rouge">2836480</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>immediately after <code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
          <li>required both:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">cdfile.size == 2836480</code></li>
              <li><code class="language-plaintext highlighter-rouge">CdPosToInt(&amp;cdfile.pos) == 3652</code></li>
            </ul>
          </li>
          <li>else returned <code class="language-plaintext highlighter-rouge">0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-pack-cdfile-pos-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the earliest exposed runtime fault is now stronger than “wrong size” alone</li>
      <li><code class="language-plaintext highlighter-rouge">CdSearchFile</code> is resolving <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> with metadata that does not match the built image’s actual ISO entry</li>
      <li>that points the live seam at:
        <ul>
          <li>path/file resolution state inside the PS1 runtime / emulator path</li>
          <li>or conflicting disc-image lookup behavior for <code class="language-plaintext highlighter-rouge">PACKS\\ACTIVITY.PAK;1</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">cdfile.size + CdPosToInt(...)</code> proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect path-resolution behavior for <code class="language-plaintext highlighter-rouge">PACKS\\ACTIVITY.PAK;1</code> specifically</li>
      <li>and compare against nearby known-good lookups to determine whether this is:
        <ul>
          <li>a pack-path-specific lookup fault</li>
          <li>or a broader CD file-resolution problem</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="0120-pdt---resetting-cd-state-immediately-before-activitypak-lookup-does-not-recover-the-scene">01:20 PDT - Resetting CD state immediately before <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup does not recover the scene</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code> <code class="language-plaintext highlighter-rouge">ps1PilotLoadPackIndex(...)</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
        <ul>
          <li>called:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">cdromResetState()</code></li>
            </ul>
          </li>
          <li>immediately before:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">CdSearchFile(&amp;cdfile, "\\PACKS\\ACTIVITY.PAK;1")</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-packsearch-reset-proof</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still changed back to the old settled-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>stale CD state alone is not sufficient to explain the bad <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> lookup</li>
      <li>the live seam remains at or above the pack-specific <code class="language-plaintext highlighter-rouge">CdSearchFile</code> resolution path itself</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary reset-before-search proof after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>compare exact <code class="language-plaintext highlighter-rouge">CdSearchFile</code> resolution behavior for <code class="language-plaintext highlighter-rouge">ACTIVITY.PAK</code> against another known-good pack in the same image</li>
      <li>to separate:
        <ul>
          <li>ACTIVITY-pack-specific lookup failure</li>
          <li>from broader pack-directory/file-resolution corruption</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2258-pdt---activity-4-on-6dae8410-story-single-was-broken-on-ps1-now-narrowed-to-exact-scene-no-launch">22:58 PDT - ACTIVITY 4 on <code class="language-plaintext highlighter-rouge">6dae8410</code>: <code class="language-plaintext highlighter-rouge">story single</code> was broken on PS1; now narrowed to exact-scene no-launch</h2>

<ul>
  <li>Goal:
    <ul>
      <li>continue scene-by-scene PS1 validation from the older <code class="language-plaintext highlighter-rouge">6dae8410</code> base and prove whether <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> was actually honoring the canonical <code class="language-plaintext highlighter-rouge">story single 4</code> boot path</li>
    </ul>
  </li>
  <li>Context:
    <ul>
      <li>older validation base:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae</code></li>
        </ul>
      </li>
      <li>target scene:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY 4</code></li>
        </ul>
      </li>
      <li>canonical boot string:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">story single 4</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>What I found:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/jc_reborn.c</code>, the PS1 <code class="language-plaintext highlighter-rouge">BOOTMODE.TXT</code> parser only handled:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">story ads ...</code></li>
        </ul>
      </li>
      <li>it silently ignored:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">story single ...</code></li>
        </ul>
      </li>
      <li>that explains the earlier PS1 drift into unrelated <code class="language-plaintext highlighter-rouge">WALKSTUF</code> / <code class="language-plaintext highlighter-rouge">FISHING</code>-like content on <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code></li>
    </ul>
  </li>
  <li>Fix applied in old worktree:
    <ul>
      <li>added:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storySetBootSceneByIndex(int)</code> in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/story.c</code></li>
        </ul>
      </li>
      <li>declared it in:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/story.h</code></li>
        </ul>
      </li>
      <li>taught PS1 boot parsing to accept:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">story single &lt;index&gt;</code></li>
          <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/jc_reborn.c</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-singlefix</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the scene shape changed immediately and materially</li>
      <li>before fix:
        <ul>
          <li>long drift into <code class="language-plaintext highlighter-rouge">WALKSTUF</code>-looking ocean and then stable <code class="language-plaintext highlighter-rouge">FISHING</code>-like island content</li>
        </ul>
      </li>
      <li>after fix:
        <ul>
          <li>no unrelated later scene</li>
          <li>run stays in title / black</li>
          <li><code class="language-plaintext highlighter-rouge">launched = false</code></li>
          <li><code class="language-plaintext highlighter-rouge">likely_scene_not_started = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">likely_visual_broken = true</code></li>
        </ul>
      </li>
      <li>this is useful progress:
        <ul>
          <li>the old random-scene misroute was real</li>
          <li>the PS1 run is now exercising the intended exact <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Follow-up check:
    <ul>
      <li>ported the static <code class="language-plaintext highlighter-rouge">adsTags</code> lifetime fix into the same old base:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code></li>
        </ul>
      </li>
      <li>validation run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-singlefix-staticadstags</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>A/B result:
    <ul>
      <li>representative frame hashes were byte-identical before / after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00250</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
      <li>so the <code class="language-plaintext highlighter-rouge">adsTags</code> heap-lifetime bug that moved <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> on current <code class="language-plaintext highlighter-rouge">HEAD</code> is not what blocks <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code></li>
    </ul>
  </li>
  <li>Current read:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">6dae8410</code> remains the better PS1 validation base</li>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> no longer looks like a random story-family reroute</li>
      <li>the active bug is now narrower:
        <ul>
          <li>exact <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> canonical boot path is requested</li>
          <li>but it still fails as a no-launch / title-black bootstrap path on this older base</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>debug the exact-scene bootstrap for <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> after the now-correct <code class="language-plaintext highlighter-rouge">story single 4</code> handoff, instead of chasing random later scene families</li>
    </ul>
  </li>
</ul>

<h2 id="2307-pdt---direct-ads-activityads-4-matches-story-single-4-byte-for-byte-on-key-frames">23:07 PDT - Direct <code class="language-plaintext highlighter-rouge">ads ACTIVITY.ADS 4</code> matches <code class="language-plaintext highlighter-rouge">story single 4</code> byte-for-byte on key frames</h2>

<ul>
  <li>Goal:
    <ul>
      <li>determine whether the remaining <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> failure on <code class="language-plaintext highlighter-rouge">6dae8410</code> is still in top-level story handoff, or already inside exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> playback/bootstrap</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4</code></li>
      <li>boot string:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Comparison target:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-singlefix</code></li>
      <li>boot string:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">story single 4</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>representative captured frames are byte-identical between the two runs:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
      <li>so the remaining <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> failure is not in the <code class="language-plaintext highlighter-rouge">story single</code> wrapper anymore</li>
      <li>it reproduces even when we bypass story selection and boot exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> directly</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">story single</code> parser bug was real and fixed</li>
      <li>but the current blocker is now deeper:
        <ul>
          <li>exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> bootstrap / playback itself</li>
        </ul>
      </li>
      <li>this is the right kind of narrowing for scene-by-scene PS1 debugging</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the first exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> bootstrap path directly:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
          <li>first <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code></li>
          <li>startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(...)</code> thread creation / survival</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2314-pdt---activityads-4-is-not-the-same-first-chunk-zero-thread-failure-shape-as-activity-1">23:14 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> is not the same first-chunk zero-thread failure shape as <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></h2>

<ul>
  <li>Goal:
    <ul>
      <li>test whether exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code> is failing in the same way <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> did earlier:
        <ul>
          <li>first <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code> returns with <code class="language-plaintext highlighter-rouge">numThreads == 0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code></li>
      <li>immediately after the first:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlayChunk(data, dataSize, offset);</code></li>
        </ul>
      </li>
      <li>for exact:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code></li>
          <li>tag <code class="language-plaintext highlighter-rouge">4</code></li>
        </ul>
      </li>
      <li>if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">numThreads == 0</code></li>
          <li>and <code class="language-plaintext highlighter-rouge">!adsStopRequested</code></li>
        </ul>
      </li>
      <li>then:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland();</code></li>
          <li><code class="language-plaintext highlighter-rouge">return;</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-firstchunk-nolaunch</code></li>
    </ul>
  </li>
  <li>Comparison target:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>representative frames remained byte-identical:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
      <li>so the temporary first-chunk no-launch hook did not fire in any way that changed visible behavior</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code> is not currently failing with the same exact first-chunk <code class="language-plaintext highlighter-rouge">numThreads == 0</code> signature that previously helped pin <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
      <li>the remaining exact-scene bootstrap failure is subtler than that:
        <ul>
          <li>either startup threads exist briefly and die later</li>
          <li>or the failure is happening on a different branch than the one just tested</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary first-chunk no-launch proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move one step deeper into exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> startup:
        <ul>
          <li>inspect which startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(...)</code> calls actually execute</li>
          <li>and whether those startup threads survive into the first main ADS loop iteration</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2320-pdt---activityads-4-startup-chain-proof-is-also-a-clean-negative">23:20 PDT - <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> startup-chain proof is also a clean negative</h2>

<ul>
  <li>Goal:
    <ul>
      <li>determine whether exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code> is reaching its authored startup chain in a way that affects the visible PS1 run</li>
    </ul>
  </li>
  <li>Authored startup sequence extracted from <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> tag <code class="language-plaintext highlighter-rouge">4</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ADD_SCENE(2,1)</code></li>
      <li><code class="language-plaintext highlighter-rouge">IF_LASTPLAYED 2,1 -&gt; ADD_SCENE(2,3)</code></li>
      <li><code class="language-plaintext highlighter-rouge">IF_LASTPLAYED 2,3 -&gt; ADD_SCENE(2,2)</code></li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code></li>
      <li>added a trap in <code class="language-plaintext highlighter-rouge">adsAddScene(...)</code></li>
      <li>for exact:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code></li>
          <li>tag <code class="language-plaintext highlighter-rouge">4</code></li>
        </ul>
      </li>
      <li>if startup scenes:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">(2,1)</code></li>
          <li><code class="language-plaintext highlighter-rouge">(2,2)</code></li>
          <li><code class="language-plaintext highlighter-rouge">(2,3)</code></li>
          <li>were being added</li>
        </ul>
      </li>
      <li>then:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland();</code></li>
          <li><code class="language-plaintext highlighter-rouge">return;</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-startupchain-proof</code></li>
    </ul>
  </li>
  <li>Comparison target:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>representative frames remained byte-identical:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
      <li>so this authored startup-chain trap did not move the visible run</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>on the current exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> PS1 path, we are not reaching the authored <code class="language-plaintext highlighter-rouge">(2,1) -&gt; (2,3) -&gt; (2,2)</code> startup chain in any way that affects the visible outcome</li>
      <li>combined with the earlier first-chunk no-launch negative, the live boundary moves earlier again:
        <ul>
          <li>before visible startup thread creation</li>
          <li>likely in exact bootstrap resource/tag resolution or very-early chunk/control flow</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary startup-chain trap after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the exact bootstrap state before visible startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(...)</code> activity:
        <ul>
          <li>tag resolution</li>
          <li>first-chunk control flow</li>
          <li>resource/slot binding for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2331-pdt---post-ttmloadttm-slot-2-visual-proofs-are-also-negative-for-activityads-4">23:31 PDT - Post-<code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> slot-2 visual proofs are also negative for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code></h2>

<ul>
  <li>Goal:
    <ul>
      <li>test whether exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code> has an obviously wrong slot-2 resource binding immediately after the TTM load loop</li>
    </ul>
  </li>
  <li>Context:
    <ul>
      <li>authored startup for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> tag <code class="language-plaintext highlighter-rouge">4</code> only touches slot <code class="language-plaintext highlighter-rouge">2</code></li>
      <li>so slot <code class="language-plaintext highlighter-rouge">2</code> is the highest-value binding to probe first</li>
    </ul>
  </li>
  <li>Temporary proofs tried in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code> immediately after:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ttmLoadTtm(&amp;ttmSlots[adsResource-&gt;res[i].id], adsResource-&gt;res[i].name);</code></li>
    </ul>
  </li>
  <li>Proof variants:
    <ul>
      <li>if slot <code class="language-plaintext highlighter-rouge">2</code> is correctly bound to <code class="language-plaintext highlighter-rouge">MJDIVE.TTM</code>, force a visible return</li>
      <li>if slot <code class="language-plaintext highlighter-rouge">2</code> is null / wrong, force a visible <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> load plus <code class="language-plaintext highlighter-rouge">grUpdateDisplay(...)</code> before return</li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-slot2-bind-proof</code></li>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-slot2-nightproof</code></li>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-slot2-nullproof</code></li>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-slot2-nullproof2</code></li>
    </ul>
  </li>
  <li>Comparison target:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>representative frames remained byte-identical across all variants:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the remaining exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> failure is earlier than every post-<code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> visual proof tried so far</li>
      <li>these probes did not produce a visible state change, so they are not good enough as the next proof surface</li>
      <li>current best boundary remains:
        <ul>
          <li>exact ACTIVITY path is requested</li>
          <li>direct <code class="language-plaintext highlighter-rouge">ads ACTIVITY.ADS 4</code> matches <code class="language-plaintext highlighter-rouge">story single 4</code></li>
          <li>but the visible failure persists before any startup-thread or post-load resource proof has produced a perturbation</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary slot-2 visual proof patches</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move earlier than the post-load visual probes:
        <ul>
          <li>exact tag-offset resolution in <code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
          <li>or pre-display bootstrap control flow that still leaves the title surface intact</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2339-pdt---even-a-top-of-adsplayactivityads-4-visible-return-is-inert-on-the-captured-output">23:39 PDT - Even a top-of-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 4)</code> visible return is inert on the captured output</h2>

<ul>
  <li>Goal:
    <ul>
      <li>verify whether the current title-like captured output is even a trustworthy proof surface for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> playback on <code class="language-plaintext highlighter-rouge">6dae8410</code></li>
    </ul>
  </li>
  <li>Strong sanity-check proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code></li>
      <li>at the top of exact:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 4)</code></li>
        </ul>
      </li>
      <li>forced:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code></li>
          <li><code class="language-plaintext highlighter-rouge">grUpdateDelay = 100</code></li>
          <li><code class="language-plaintext highlighter-rouge">grUpdateDisplay(NULL, ttmThreads, NULL)</code></li>
          <li>immediate <code class="language-plaintext highlighter-rouge">return</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-adsplay-nightproof</code></li>
    </ul>
  </li>
  <li>Comparison target:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>representative frames remained byte-identical:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_00310</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_00660</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_01200</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the current title-like captured output is not a trustworthy proof surface for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> playback on this older base</li>
      <li>after this many inert visible proofs, continuing to infer control flow from the stable title surface is no longer defensible</li>
      <li>the debugging strategy needs to change:
        <ul>
          <li>stop relying on visible-frame perturbations at this boundary</li>
          <li>move to a different runtime-state proof surface</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary top-of-<code class="language-plaintext highlighter-rouge">adsPlay()</code> proof patch</li>
      <li>reverted the temporary complementary tag-offset proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>switch <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> debugging to a non-visual state surface on <code class="language-plaintext highlighter-rouge">6dae8410</code></li>
      <li>likely candidates:
        <ul>
          <li>runtime state hash / RAM hash transitions</li>
          <li>durable story / ADS state persisted into telemetry in a form that actually survives on this branch</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2345-pdt---non-visual-validation-confirmed-exact-adsplayactivityads-4-proof-does-execute-even-though-frames-do-not-move">23:45 PDT - Non-visual validation confirmed: exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 4)</code> proof does execute even though frames do not move</h2>

<ul>
  <li>Goal:
    <ul>
      <li>verify that the inert frame proofs on <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> were not simply dead code paths</li>
    </ul>
  </li>
  <li>Comparison:
    <ul>
      <li>baseline direct exact ADS run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-directads4/printf.log</code></li>
        </ul>
      </li>
      <li>top-of-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 4)</code> proof run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-adsplay-nightproof/printf.log</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>captured frames stayed byte-identical, but the non-visual runtime surface changed substantially</li>
      <li>baseline:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">Save State Hash = 048bcd0b774df0f7099975e33e26364b19cb2c6e3b6010e3a838b899b241d7c4</code></li>
          <li><code class="language-plaintext highlighter-rouge">RAM Hash = c492897409507dcb852cab1f8886ed64cc3efe94fa7341fd663ca207c1eb781a</code></li>
          <li><code class="language-plaintext highlighter-rouge">Total execution time = 2545.77 ms</code></li>
        </ul>
      </li>
      <li>proof run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">Save State Hash = de10700000100da320b4b3366e475d78a1b95c1571c864067f169d3b532a602c</code></li>
          <li><code class="language-plaintext highlighter-rouge">RAM Hash = 5c0939e6a6c10e1b2e8fa6e54f8de2973a651810f44b112e8569c172a49c3d91</code></li>
          <li><code class="language-plaintext highlighter-rouge">Total execution time = 3782.95 ms</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">VRAM Hash</code> stayed the same</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 4</code> playback is executing on <code class="language-plaintext highlighter-rouge">6dae8410</code></li>
      <li>the current captured frame surface is simply not trustworthy at this boundary</li>
      <li><code class="language-plaintext highlighter-rouge">RAM Hash</code> / <code class="language-plaintext highlighter-rouge">Save State Hash</code> / execution-time changes are a better proof surface here than the PNGs</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>continue <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> using non-visual runtime-state proofs first</li>
      <li>only use frames secondarily, once a proof is known to touch VRAM or display ownership</li>
    </ul>
  </li>
</ul>

<h2 id="2346-pdt---fresh-build-timeout-proof-shows-island-ads-activityads-4-is-not-reaching-mains-argads-branch-on-6dae8410">23:46 PDT - Fresh-build timeout proof shows <code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code> is not reaching <code class="language-plaintext highlighter-rouge">main()</code>’s <code class="language-plaintext highlighter-rouge">argAds</code> branch on <code class="language-plaintext highlighter-rouge">6dae8410</code></h2>

<ul>
  <li>Goal:
    <ul>
      <li>verify the actual entrypoint for the old-base “direct ADS” runs, because earlier ADS-level timeout proofs were exiting cleanly even after fresh rebuilds</li>
    </ul>
  </li>
  <li>Fresh-build check:
    <ul>
      <li>touched <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/jc_reborn.c</code></li>
      <li>confirmed rebuild picked it up:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">jc_reborn.c.obj</code></li>
          <li><code class="language-plaintext highlighter-rouge">jcreborn.exe</code></li>
          <li>all rebuilt at <code class="language-plaintext highlighter-rouge">23:34</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/jc_reborn.c</code></li>
      <li>at the top of:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">else if (argAds &amp;&amp; numArgs &gt;= 2)</code></li>
        </ul>
      </li>
      <li>inserted:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">while (1) { }</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-main-anyargads-timeoutproof</code></li>
      <li>boot string:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code></li>
        </ul>
      </li>
      <li>timeout budget:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">15s</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run still exited cleanly</li>
      <li>it did not time out</li>
      <li>so the old-base direct-ADS boot string is not reaching <code class="language-plaintext highlighter-rouge">main()</code>’s <code class="language-plaintext highlighter-rouge">argAds</code> branch</li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>previous “direct ADS” runs on <code class="language-plaintext highlighter-rouge">6dae8410</code> were not actually proving ADS-branch entry</li>
      <li>that invalidates the earlier assumption that <code class="language-plaintext highlighter-rouge">island ads ACTIVITY.ADS 4</code> was already inside exact ADS playback on this branch</li>
      <li>the active root-cause surface moves up:
        <ul>
          <li>PS1 boot-override parsing / retention / dispatch for <code class="language-plaintext highlighter-rouge">island ads ...</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">argAds</code> timeout proof in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/jc_reborn.c</code></li>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> timeout proof in <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/ads.c</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect why <code class="language-plaintext highlighter-rouge">island ads ...</code> is not reaching <code class="language-plaintext highlighter-rouge">argAds</code> on <code class="language-plaintext highlighter-rouge">6dae8410</code></li>
      <li>specifically:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ps1LoadBootOverride()</code></li>
          <li><code class="language-plaintext highlighter-rouge">ps1ApplyBootOverride()</code></li>
          <li><code class="language-plaintext highlighter-rouge">argPlayAll</code> / <code class="language-plaintext highlighter-rouge">argAds</code> / <code class="language-plaintext highlighter-rouge">numArgs</code> state after parsing</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2154-pdt---adsload-heap-allocation-was-corrupting-activity-bootstrap-static-ads-tag-storage-removes-the-old-ocean-failure">21:54 PDT - <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> heap allocation was corrupting ACTIVITY bootstrap; static ADS tag storage removes the old ocean failure</h2>

<ul>
  <li>First exact-bytes proof:
    <ul>
      <li>temporary proof in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code> blacked only if the first exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> load saw:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">dataSize == 2558</code></li>
          <li>the expected extracted byte prefix</li>
        </ul>
      </li>
      <li>validation:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-ads-bytes-firstproof/20260328-214718/frames/jcreborn/frame_04910.png</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>boundary frames blacked:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>so the first exact PS1 <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> payload bytes are already correct before <code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
    </ul>
  </li>
  <li>Allocation-vs-scan split:
    <ul>
      <li>replaced the temporary <code class="language-plaintext highlighter-rouge">adsTags = safe_malloc(...)</code> allocation with a static scratch buffer only for the first exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> load</li>
      <li>blacked only if <code class="language-plaintext highlighter-rouge">ttmSlots[1].ttmResource == GJDIVE.TTM</code> still held immediately after <code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
      <li>validation:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-adsload-static-tags/20260328-215017/frames/jcreborn/frame_04910.png</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the boundary frames blacked again</li>
      <li>so the active corruption surface was the <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> heap allocation path, not ACTIVITY byte delivery and not the ADS scan logic alone</li>
    </ul>
  </li>
  <li>Current code change:
    <ul>
      <li>converted PS1 <code class="language-plaintext highlighter-rouge">adsTags</code> storage in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code> from per-play heap allocation to a persistent static buffer with capacity guard:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">PS1_STATIC_ADS_TAG_CAPACITY = 1024</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Clean validation run with the real change:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-static-adstags-fix/20260328-215232/frames/jcreborn/frame_04910.png</code></li>
    </ul>
  </li>
  <li>Clean longer validation:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-static-adstags-fix-6300/20260328-215318/frames/jcreborn/frame_06000.png</code></li>
    </ul>
  </li>
  <li>Important result:
    <ul>
      <li>the old settled ACTIVITY ocean hash is gone</li>
      <li>boundary frames now stay black through the old failure onset:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04920 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>later frames no longer match the old ocean either:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_06000 = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06150 = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_06300 = 91d3e49ac1712390a97e7588519fa53d29b3a1cf33ae1f1696705a097ddb534d</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current conclusion:
    <ul>
      <li>this is the first material PS1 runtime fix on <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code></li>
      <li>the <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> heap allocation really was corrupting bootstrap state</li>
      <li>but <code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> is not fully fixed yet; it has moved from the old settled-ocean failure to a new later failure surface</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>characterize the new <code class="language-plaintext highlighter-rouge">06000+</code> stable state after the static-<code class="language-plaintext highlighter-rouge">adsTags</code> fix</li>
      <li>then continue scene-by-scene debugging from that cleaner post-bootstrap runtime</li>
    </ul>
  </li>
</ul>

<h2 id="2236-pdt---step-back-validation-6dae8410-is-a-materially-better-ps1-base-than-current-head-for-activity-4">22:36 PDT - Step-back validation: <code class="language-plaintext highlighter-rouge">6dae8410</code> is a materially better PS1 base than current HEAD for <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code></h2>

<ul>
  <li>Reason for step-back:
    <ul>
      <li>current <code class="language-plaintext highlighter-rouge">ps1</code> HEAD had regressed too far for validation sweeps:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY 1</code> and <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> were collapsing to black-screen/fallback behavior</li>
        </ul>
      </li>
      <li>that made the current tree unsuitable as a “closest working” PS1 candidate</li>
    </ul>
  </li>
  <li>Validation base:
    <ul>
      <li>older detached worktree:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae</code></li>
        </ul>
      </li>
      <li>commit:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">6dae8410</code></li>
          <li><code class="language-plaintext highlighter-rouge">2026-03-21 09:02:06 -0700</code></li>
          <li><code class="language-plaintext highlighter-rouge">Fix PS1 first-handoff blackscreen</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Harness bridge:
    <ul>
      <li>copied current regtest harness scripts into the older worktree just for validation</li>
      <li>fixed that older snapshot’s stale <code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/scripts/build-ps1.sh</code>, which was still calling:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">make jcreborn</code></li>
        </ul>
      </li>
      <li>updated it to use the existing CMake-based <code class="language-plaintext highlighter-rouge">build-ps1/</code> tree</li>
    </ul>
  </li>
  <li>One-scene validation:
    <ul>
      <li>ran:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ACTIVITY 4</code></li>
          <li><code class="language-plaintext highlighter-rouge">4200</code> frames</li>
          <li><code class="language-plaintext highlighter-rouge">Interpreter</code></li>
        </ul>
      </li>
      <li>output:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/tmp/jc_reborn_6dae/regtest-results/activity4-6dae-check/result.json</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>older <code class="language-plaintext highlighter-rouge">6dae8410</code> snapshot reaches real non-black scene content</li>
      <li>last visual scene on that run:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">screen_type = island</code></li>
          <li><code class="language-plaintext highlighter-rouge">scene_family = FISHING</code></li>
          <li><code class="language-plaintext highlighter-rouge">johnny_present = true</code></li>
          <li><code class="language-plaintext highlighter-rouge">sand_present = true</code></li>
        </ul>
      </li>
      <li>hashes:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = c979c7cb2a17e672791991117ed14777f3441fe6b074cd69f5683189d7435167</code></li>
          <li>best scene frame:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">frame_03150.png</code></li>
              <li><code class="language-plaintext highlighter-rouge">frame_scene_pixel_sha256 = 76de45bc2265ba5180709b2273f5a5f655459a3072d30b27d45c23de9f195426</code></li>
            </ul>
          </li>
          <li>last scene frame:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">frame_04200.png</code></li>
              <li><code class="language-plaintext highlighter-rouge">frame_scene_pixel_sha256 = 54a1f25fab3e3fa0ae236af60b06c022d556d8bdd686cfedff8104e1f860b9bf</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>even before the exact compare JSON is finalized, this is already enough to choose direction:</li>
      <li><code class="language-plaintext highlighter-rouge">6dae8410</code> is materially healthier than current HEAD for one-scene validation</li>
      <li>current HEAD should not be used as the candidate PS1 sweep/debug base</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>keep validating from <code class="language-plaintext highlighter-rouge">6dae8410</code> one scene at a time</li>
      <li>confirm the exact <code class="language-plaintext highlighter-rouge">ACTIVITY 4</code> compare verdict when the compare tool completes</li>
      <li>if that remains the best behavior, continue scene debugging from the older base instead of current HEAD</li>
    </ul>
  </li>
</ul>

<h2 id="2120-pdt---activity-startup-failure-is-now-narrowed-to-missing-slot-1-resource-binding-before-first-chunk-playback">21:20 PDT - ACTIVITY startup failure is now narrowed to missing slot-1 resource binding before first chunk playback</h2>

<ul>
  <li>Temporary proof family in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>:
    <ul>
      <li>black-and-return only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> under progressively narrower conditions</li>
      <li>all probes were run under <code class="language-plaintext highlighter-rouge">--cpu Interpreter</code> to avoid DuckStation recompiler crashes</li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-startup-survival-proof-interp/20260328-211458/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-startup-zeroip-only/20260328-211613/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-startup-slot1-ttmproof/20260328-211819/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-startup-slot1-nullproof/20260328-211918/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-slot1-prechunk-nullproof/20260328-212046/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-slot1-resource-list-proof/20260328-212156/frames/jcreborn/frame_04910.png</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>every proof above drove <code class="language-plaintext highlighter-rouge">frame_04900</code>, <code class="language-plaintext highlighter-rouge">frame_04910</code>, and <code class="language-plaintext highlighter-rouge">frame_05000</code> to the same black hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusions:
    <ul>
      <li>the first exact ACTIVITY chunk really does reach authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,12)</code> / <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,13)</code></li>
      <li>a zero-<code class="language-plaintext highlighter-rouge">ip</code> birth proof alone is sufficient to black out the stable ocean band</li>
      <li>by <code class="language-plaintext highlighter-rouge">adsAddScene(1,12/13)</code>, slot <code class="language-plaintext highlighter-rouge">1</code> is not bound to live <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code></li>
      <li>a null-only slot-1 proof also blacks the band, so this is not just “wrong non-null TTM”; slot <code class="language-plaintext highlighter-rouge">1</code> is effectively unbound</li>
      <li>moving the proof earlier, immediately after the <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> loop and before first-chunk playback, still blacks the band</li>
      <li>finally, proving only that the parsed ACTIVITY resource list lacks <code class="language-plaintext highlighter-rouge">slot 1 -&gt; GJDIVE.TTM</code> also blacks the band</li>
    </ul>
  </li>
  <li>Current best read:
    <ul>
      <li>the ACTIVITY failure is now upstream of scene-thread lifetime</li>
      <li>on PS1, the parsed <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> resource mapping reaching runtime does not preserve the expected <code class="language-plaintext highlighter-rouge">slot 1 -&gt; GJDIVE.TTM</code> binding</li>
      <li>that explains why authored startup tags <code class="language-plaintext highlighter-rouge">12/13</code> become <code class="language-plaintext highlighter-rouge">ip == 0</code> at runtime and no ACTIVITY startup thread survives first-chunk bootstrap</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary proof patches after validation; only the worklog entry remains</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect why PS1 <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> resource metadata loses <code class="language-plaintext highlighter-rouge">slot 1 -&gt; GJDIVE.TTM</code></li>
      <li>likely surfaces:
        <ul>
          <li>PS1 ADS resource parse/list integrity</li>
          <li>ACTIVITY resource-list contents at runtime versus expected authored mapping</li>
          <li>any slot reset/overwrite before first chunk playback</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2135-pdt---raw-resource001-parse-confirms-activity-source-asset-is-correct">21:35 PDT - Raw <code class="language-plaintext highlighter-rouge">RESOURCE.001</code> parse confirms ACTIVITY source asset is correct</h2>

<ul>
  <li>Parsed the on-disk <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code> entry directly from:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/jc_resources/RESOURCE.MAP</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/jc_resources/RESOURCE.001</code></li>
    </ul>
  </li>
  <li>Important detail:
    <ul>
      <li>the ADS <code class="language-plaintext highlighter-rouge">RES:</code> table uses variable-length NUL-terminated names, matching <code class="language-plaintext highlighter-rouge">getString(..., 40)</code> / <code class="language-plaintext highlighter-rouge">ps1_getString(..., 40)</code>, not fixed 40-byte records</li>
    </ul>
  </li>
  <li>Direct raw result for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">numRes = 6</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 1 -&gt; GJDIVE.TTM</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 2 -&gt; MJDIVE.TTM</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 4 -&gt; MJREAD.TTM</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 5 -&gt; MJBATH.TTM</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 6 -&gt; GJNAT1.TTM</code></li>
      <li><code class="language-plaintext highlighter-rouge">slot 7 -&gt; GJNAT3.TTM</code></li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the source asset on disk is correct</li>
      <li>the failure is not in authored ACTIVITY metadata</li>
      <li>the remaining bug surface is after bytes leave disk:
        <ul>
          <li>PS1 runtime parse/in-memory ADS resource state</li>
          <li>or later slot reset/overwrite before first chunk playback</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted an unstable first-entry positive proof patch after it failed to produce a clean capture window</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect live in-memory <code class="language-plaintext highlighter-rouge">adsResource-&gt;numRes/res[]</code> state on the PS1 path without relying on repeated-entry proofs</li>
      <li>then inspect whether any slot reset occurs between <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> and the first startup <code class="language-plaintext highlighter-rouge">adsAddScene(1,12/13)</code></li>
    </ul>
  </li>
</ul>

<h2 id="2145-pdt---first-exact-activity-in-memory-ads-list-and-slot-1-ttm-load-are-both-correct">21:45 PDT - First exact ACTIVITY in-memory ADS list and slot-1 TTM load are both correct</h2>

<ul>
  <li>Temporary first-entry proofs in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, all under <code class="language-plaintext highlighter-rouge">--cpu Interpreter</code>:
    <ul>
      <li>black-and-return only if the first exact <code class="language-plaintext highlighter-rouge">adsPlay("ACTIVITY.ADS", 1)</code> sees the full expected six-entry in-memory <code class="language-plaintext highlighter-rouge">adsResource-&gt;res[]</code> mapping</li>
      <li>then black-and-return only if that same first exact path sees <code class="language-plaintext highlighter-rouge">ttmSlots[1].ttmResource == GJDIVE.TTM</code> immediately after the <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> loop</li>
    </ul>
  </li>
  <li>Validation runs:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-reslist-firstproof/20260328-213640/frames/jcreborn/frame_04910.png</code></li>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-slot1-load-firstproof/20260328-213740/frames/jcreborn/frame_04910.png</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>both proofs turned <code class="language-plaintext highlighter-rouge">frame_04900</code>, <code class="language-plaintext highlighter-rouge">frame_04910</code>, and <code class="language-plaintext highlighter-rouge">frame_05000</code> black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>on the first exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> playback:
        <ul>
          <li>the in-memory ADS resource list is already correct</li>
          <li>and <code class="language-plaintext highlighter-rouge">ttmSlots[1]</code> is correctly bound to <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code> immediately after the load loop</li>
        </ul>
      </li>
      <li>so the slot-1 loss does <strong>not</strong> happen in:
        <ul>
          <li>source asset metadata</li>
          <li>PS1 ADS resource parsing</li>
          <li>or the initial <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> binding step</li>
        </ul>
      </li>
      <li>the remaining live boundary is now very tight:
        <ul>
          <li>between the end of the <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> loop and the first startup <code class="language-plaintext highlighter-rouge">adsAddScene(1,12/13)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary first-entry proof patches after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect what mutates or invalidates slot <code class="language-plaintext highlighter-rouge">1</code> in that narrow pre-chunk bootstrap span</li>
      <li>likely surfaces:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code></li>
          <li>or a slot reset/overwrite side effect before <code class="language-plaintext highlighter-rouge">adsAddScene(1,12/13)</code> runs</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2155-pdt---adsload-is-the-active-boundary-but-not-via-simple-tagchunk-table-overflow">21:55 PDT - <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> is the active boundary, but not via simple tag/chunk table overflow</h2>

<ul>
  <li>Temporary boundary proof in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>:
    <ul>
      <li>on the first exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> playback only</li>
      <li>black-and-return immediately after <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> if <code class="language-plaintext highlighter-rouge">ttmSlots[1]</code> is already not bound to <code class="language-plaintext highlighter-rouge">GJDIVE.TTM</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-prechunk-slot1-loss/20260328-214146/frames/jcreborn/frame_04910.png</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code>, <code class="language-plaintext highlighter-rouge">frame_04910</code>, <code class="language-plaintext highlighter-rouge">frame_05000</code> all black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the slot-1 loss is already present immediately after <code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
      <li>so the current corruption boundary is:
        <ul>
          <li>after <code class="language-plaintext highlighter-rouge">ttmLoadTtm()</code> succeeds for <code class="language-plaintext highlighter-rouge">slot 1 -&gt; GJDIVE.TTM</code></li>
          <li>but before the first <code class="language-plaintext highlighter-rouge">adsPlayChunk(...)</code> executes ACTIVITY bytecode</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Follow-up static validation using <code class="language-plaintext highlighter-rouge">repo:/jc_resources/extracted/ads/ACTIVITY.ADS</code>:
    <ul>
      <li>simulated the exact <code class="language-plaintext highlighter-rouge">adsLoad(...)</code> scanner on the real decompressed ACTIVITY bytecode</li>
      <li>observed:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNumTags = 10</code>, matching metadata from <code class="language-plaintext highlighter-rouge">RESOURCE.001</code></li>
          <li>bookmarked chunk counts stay well below <code class="language-plaintext highlighter-rouge">MAX_ADS_CHUNKS</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important refinement:
    <ul>
      <li>this is not explained by a simple <code class="language-plaintext highlighter-rouge">adsTags</code> overflow</li>
      <li>and not by a simple <code class="language-plaintext highlighter-rouge">adsChunks</code> overflow</li>
      <li><code class="language-plaintext highlighter-rouge">adsLoad(...)</code> remains the active boundary, but the failure is subtler than straightforward table overrun from ACTIVITY bytecode shape</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsLoad(...)</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect heap/object lifetime side effects inside <code class="language-plaintext highlighter-rouge">adsLoad(...)</code></li>
      <li>especially:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsTags = safe_malloc(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsReleaseAds()/free(adsTags)</code> lifetime assumptions</li>
          <li>any corruption caused by repeated ACTIVITY exact-entry bookkeeping versus allocator reuse</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2016-pdt---quadrant-color-upload-proof-collapsed-the-late-activity-frames-to-near-black-but-did-not-identify-tile-ownership">20:16 PDT - Quadrant-color upload proof collapsed the late ACTIVITY frames to near-black, but did not identify tile ownership</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, replaced the normal late <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload source for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> during <code class="language-plaintext highlighter-rouge">04900..04910</code></li>
      <li>each quadrant tile uploaded from a different solid-color buffer:
        <ul>
          <li>top-left: red</li>
          <li>top-right: green</li>
          <li>bottom-left: blue</li>
          <li>bottom-right: white</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-quadrant-proof-4900-4910</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>late sampled frames all collapsed to the same new hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = 1c7b028eeb1335dc37111468c89ff4c9f2194efe3645fae8656d5114bebf52b5</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 1c7b028eeb1335dc37111468c89ff4c9f2194efe3645fae8656d5114bebf52b5</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 1c7b028eeb1335dc37111468c89ff4c9f2194efe3645fae8656d5114bebf52b5</code></li>
        </ul>
      </li>
      <li>simple quadrant averages on <code class="language-plaintext highlighter-rouge">frame_04910</code> were all essentially black:
        <ul>
          <li>top-left: <code class="language-plaintext highlighter-rouge">(0, 0, 1)</code></li>
          <li>top-right: <code class="language-plaintext highlighter-rouge">(0, 0, 0)</code></li>
          <li>bottom-left: <code class="language-plaintext highlighter-rouge">(0, 0, 0)</code></li>
          <li>bottom-right: <code class="language-plaintext highlighter-rouge">(0, 0, 0)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this was not informative enough to identify which tile/quadrant owns the settled ACTIVITY ocean</li>
      <li>the proof only showed that substituting the late upload source in this way can collapse the frame to near-black</li>
      <li>the next useful surface remains the real normal upload-source content at <code class="language-plaintext highlighter-rouge">04910</code>, not another quadrant substitution</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary quadrant-color proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect or checksum the real <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> tile-upload source content at the <code class="language-plaintext highlighter-rouge">04910</code> handoff window</li>
    </ul>
  </li>
</ul>

<h2 id="2028-pdt---by-0490005000-no-normal-background-tile-uploads-are-active-anymore-on-the-stable-activity-failure-path">20:28 PDT - By <code class="language-plaintext highlighter-rouge">04900..05000</code>, no normal background-tile uploads are active anymore on the stable ACTIVITY failure path</h2>

<ul>
  <li>Temporary telemetry:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, added compact-strip values for:
        <ul>
          <li>per-tile <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload source signatures</li>
          <li>an upload-active bitmask showing which of the four background tiles actually reached <code class="language-plaintext highlighter-rouge">LoadImage(...)</code> this frame</li>
        </ul>
      </li>
      <li>decoded them in <code class="language-plaintext highlighter-rouge">repo:/scripts/decode-ps1-bars.py</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-uploadsig2</code></li>
    </ul>
  </li>
  <li>Decoded boundary frames:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">upload_active_mask_estimate = 0</code></li>
          <li>all four <code class="language-plaintext highlighter-rouge">upload_sig_tile* = 0</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">upload_active_mask_estimate = 0</code></li>
          <li>all four <code class="language-plaintext highlighter-rouge">upload_sig_tile* = 0</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code>:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">upload_active_mask_estimate = 0</code></li>
          <li>all four <code class="language-plaintext highlighter-rouge">upload_sig_tile* = 0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>by the settled bad-ocean band, <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> is no longer performing any normal per-tile uploads at all</li>
      <li>that reconciles the earlier renderer proofs:
        <ul>
          <li>forcing a black upload source only changes the result when uploads are explicitly forced into the band</li>
          <li>otherwise the visible ACTIVITY ocean is already persistent before <code class="language-plaintext highlighter-rouge">04900</code></li>
        </ul>
      </li>
      <li>so the active boundary is now earlier than the late renderer upload path</li>
      <li>the next useful target is no longer “what gets uploaded at <code class="language-plaintext highlighter-rouge">04910</code>,” but:
        <ul>
          <li>where the last successful background upload happens before the ocean becomes persistent</li>
          <li>and why no later ACTIVITY scene content dirties/uploads the framebuffer after that point</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move the renderer proof surface earlier than <code class="language-plaintext highlighter-rouge">04900</code></li>
      <li>identify the final frame where any background tile upload is still active, then debug the transition from that last upload into the settled persistent ocean</li>
    </ul>
  </li>
</ul>

<h2 id="2033-pdt---the-last-decoded-normal-background-upload-in-the-activity-run-is-back-at-frame_0610">20:33 PDT - The last decoded normal background upload in the ACTIVITY run is back at <code class="language-plaintext highlighter-rouge">frame_0610</code></h2>

<ul>
  <li>Follow-up on <code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-uploadsig2</code>:
    <ul>
      <li>scanned the full decoded telemetry timeline for the new:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">upload_active_mask_estimate</code></li>
          <li><code class="language-plaintext highlighter-rouge">upload_sig_tile*</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the last frame with any nonzero decoded upload activity is:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_0610</code></li>
        </ul>
      </li>
      <li>from <code class="language-plaintext highlighter-rouge">frame_0620</code> onward, <code class="language-plaintext highlighter-rouge">upload_active_mask_estimate = 0</code></li>
      <li>that includes the entire settled failure band:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean is not being maintained by normal <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> uploads anywhere near the failure onset</li>
      <li>by the time the run reaches the long black lead-in and then the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean, the normal background upload path is already dormant</li>
      <li>that is consistent with the earlier blackproofs:
        <ul>
          <li>forced uploads can overwrite the persistent framebuffer</li>
          <li>but the normal path is no longer uploading there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Current read:
    <ul>
      <li>the active bug is now better described as:
        <ul>
          <li>normal background upload activity stops very early</li>
          <li>then some other path leaves or restores a persistent ocean/title-derived visible framebuffer state</li>
          <li>and no later ACTIVITY scene content ever dirties/uploads over it</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop treating <code class="language-plaintext highlighter-rouge">04910</code> as an active upload site</li>
      <li>debug the direct framebuffer/background ownership path after uploads stop:
        <ul>
          <li>title/intro display persistence</li>
          <li>direct screen loads</li>
          <li>any display-page reuse that survives while the ACTIVITY scene never reclaims the framebuffer</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="2043-pdt---one-shot-post-init-framebuffer-clear-removes-the-stale-04900-title-but-not-the-settled-04910-ocean">20:43 PDT - One-shot post-init framebuffer clear removes the stale <code class="language-plaintext highlighter-rouge">04900</code> title, but not the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/jc_reborn.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only:
        <ul>
          <li>immediately after <code class="language-plaintext highlighter-rouge">graphicsInit()</code> and palette setup</li>
          <li>called:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">VSync(0)</code></li>
            </ul>
          </li>
        </ul>
      </li>
      <li>this performs a one-shot black framebuffer handoff after the early title path, before <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-postinit-clear2</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code> changed from the old late-title frame to black:
        <ul>
          <li>new hash: <code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>but the settled ACTIVITY ocean did not move:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>there really is stale pre-story framebuffer/title persistence contributing to the late <code class="language-plaintext highlighter-rouge">04900</code> frame</li>
      <li>but that is not the root cause of the settled ACTIVITY failure</li>
      <li>the later <code class="language-plaintext highlighter-rouge">04910+</code> ocean is being established independently of that stale title residue</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-init framebuffer-clear proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>treat <code class="language-plaintext highlighter-rouge">04900</code> title persistence and <code class="language-plaintext highlighter-rouge">04910+</code> settled ocean as two separate phenomena</li>
      <li>continue debugging the first point where the persistent ACTIVITY ocean itself is established</li>
    </ul>
  </li>
</ul>

<h2 id="2046-pdt---the-settled-04910-activity-ocean-comes-directly-from-the-exact-ocean0scr-bootstrap-load-in-islandinit">20:46 PDT - The settled <code class="language-plaintext highlighter-rouge">04910+</code> ACTIVITY ocean comes directly from the exact <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> bootstrap load in <code class="language-plaintext highlighter-rouge">islandInit()</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only:
        <ul>
          <li>replaced only the initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>with:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
            </ul>
          </li>
        </ul>
      </li>
      <li>left the rest of <code class="language-plaintext highlighter-rouge">islandInit()</code> intact:
        <ul>
          <li>raft/cloud/background BMP loads</li>
          <li>island sprite composition</li>
          <li>initial wave draws</li>
          <li>later ACTIVITY flow unchanged</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-skip-ocean-load</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>all three boundary frames turned black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is the strongest causal proof so far:
        <ul>
          <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ACTIVITY ocean is coming directly from the exact bootstrap <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> load in <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>it is not being independently recreated later by other island composition or fallback logic</li>
        </ul>
      </li>
      <li>that means the remaining ACTIVITY failure is now best described as:
        <ul>
          <li>the bootstrap ocean background loads successfully</li>
          <li>but the intended ACTIVITY scene content never later reclaims or replaces it</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary skip-ocean-load proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>focus directly on why exact ACTIVITY playback never replaces the already-correctly-loaded bootstrap ocean background</li>
    </ul>
  </li>
</ul>

<h2 id="2052-pdt---direct-black-and-return-at-the-top-of-storyplaypreparedsceneactivityads-1-still-does-not-move-the-settled-04910-ocean">20:52 PDT - Direct black-and-return at the top of <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> still does not move the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only:
        <ul>
          <li>at the top of <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(scene, prevSpot, prevHdg)</code></li>
          <li>called:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
            </ul>
          </li>
          <li>then returned <code class="language-plaintext highlighter-rouge">0</code> immediately</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-playprepared-blackreturn</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>lead-in changed:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
      <li>but the settled ACTIVITY ocean remained unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is another strong negative on the straightforward top-level playback path</li>
      <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean is still not being controlled by the obvious exact <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(ACTIVITY.ADS, 1)</code> hook point</li>
      <li>so the strongest remaining read is unchanged:
        <ul>
          <li>exact bootstrap ocean background is loaded correctly</li>
          <li>but the later stable failure path that leaves it on screen is bypassing, outliving, or otherwise decoupling from the straightforward exact prepared-scene playback hook</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> black-return proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move below top-level story hooks again</li>
      <li>identify the alternate ACTIVITY path that preserves the bootstrap ocean while the obvious exact prepared-scene playback hook remains behaviorally negative</li>
    </ul>
  </li>
</ul>

<h2 id="2056-pdt---even-returning-from-storyplay-immediately-after-an-exact-no-launch-still-does-not-move-the-settled-04910-ocean">20:56 PDT - Even returning from <code class="language-plaintext highlighter-rouge">storyPlay()</code> immediately after an exact no-launch still does not move the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, wrapped:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
      <li>if it returned false for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, immediately:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
          <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
          <li><code class="language-plaintext highlighter-rouge">return</code></li>
        </ul>
      </li>
      <li>this bypassed the obvious retry loop after an exact prepared-scene no-launch</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-no-launch-return</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>lead-in changed:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
      <li>but the settled ACTIVITY ocean still did not move:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ACTIVITY ocean is still not explained by the obvious exact no-launch retry path in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>this is another strong negative on the straightforward top-level story control flow</li>
      <li>the remaining failure path is still behaving as if it bypasses, outlives, or otherwise decouples from the obvious exact prepared-scene playback/no-launch branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary no-launch-return proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>keep the focus below top-level story control flow</li>
      <li>the strongest remaining suspect surface is now the ADS bootstrap/scene-thread lifecycle itself, after the bootstrap ocean background has been loaded</li>
    </ul>
  </li>
</ul>

<h2 id="2101-pdt---if-exact-activity-still-has-numthreads--0-immediately-after-the-first-adsplaychunk-the-whole-late-boundary-turns-black">21:01 PDT - If exact ACTIVITY still has <code class="language-plaintext highlighter-rouge">numThreads == 0</code> immediately after the first <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code>, the whole late boundary turns black</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only:
        <ul>
          <li>immediately after the first:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">adsPlayChunk(data, dataSize, offset)</code></li>
            </ul>
          </li>
          <li>if <code class="language-plaintext highlighter-rouge">numThreads == 0</code>, then:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-firstchunk-nolaunch-return</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>all three boundary frames turned black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is the strongest ADS bootstrap proof so far</li>
      <li>the settled ACTIVITY ocean is consistent with the exact first ADS chunk returning with no launched scene threads</li>
      <li>in other words:
        <ul>
          <li>bootstrap ocean background loads</li>
          <li>first exact <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code> does not produce any running ACTIVITY scene thread</li>
          <li>and later nothing reclaims the framebuffer from that bootstrap state</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary firstchunk-nolaunch-return proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>debug why the exact first <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code> path for <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> leaves <code class="language-plaintext highlighter-rouge">numThreads == 0</code></li>
      <li>that is now the narrowest live root-cause surface</li>
    </ul>
  </li>
</ul>

<h2 id="2106-pdt---the-first-exact-activity-chunk-definitely-reaches-authored-startup-add_scene112--add_scene113">21:06 PDT - The first exact ACTIVITY chunk definitely reaches authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,12)</code> / <code class="language-plaintext highlighter-rouge">ADD_SCENE(1,13)</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only:
        <ul>
          <li>inside <code class="language-plaintext highlighter-rouge">adsAddScene(ttmSlotNo, ttmTag, arg3)</code></li>
          <li>if the scene being added was authored startup slot/tag:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">1:12</code> or <code class="language-plaintext highlighter-rouge">1:13</code></li>
            </ul>
          </li>
          <li>then:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
              <li><code class="language-plaintext highlighter-rouge">return</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-addscene-init12-13-return</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>all three boundary frames turned black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this proves the first exact ACTIVITY chunk really is reaching the authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE</code> calls for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">slot 1 / tag 12</code></li>
          <li><code class="language-plaintext highlighter-rouge">slot 1 / tag 13</code></li>
        </ul>
      </li>
      <li>combined with the earlier first-chunk no-launch proof, the remaining root-cause surface is now extremely tight:
        <ul>
          <li>authored startup <code class="language-plaintext highlighter-rouge">ADD_SCENE</code> is reached</li>
          <li>but by the time <code class="language-plaintext highlighter-rouge">adsPlayChunk()</code> returns, no running scene thread survives (<code class="language-plaintext highlighter-rouge">numThreads == 0</code>)</li>
        </ul>
      </li>
      <li>so the bug is no longer “wrong chunk” or “startup <code class="language-plaintext highlighter-rouge">ADD_SCENE</code> never hit”</li>
      <li>it is now in the immediate lifetime of those just-added startup threads</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsAddScene(1:12/1:13)</code> proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>debug why the just-added ACTIVITY startup threads do not survive past the first-chunk bootstrap</li>
      <li>the narrowest live candidates are:
        <ul>
          <li>duplicate/running suppression</li>
          <li>immediate stop/termination in the same chunk</li>
          <li>zero/invalid TTM entrypoint after add</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1939-pdt---late-activity-background-thread-suppression-does-not-move-the-settled-ocean">19:39 PDT - Late ACTIVITY background-thread suppression does not move the settled ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, suppressed only:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">islandAnimate(&amp;ttmBackgroundThread)</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandRedrawWave(&amp;ttmBackgroundThread)</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
      <li>left all <code class="language-plaintext highlighter-rouge">ttmPlay(...)</code> scene-thread playback intact</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-bgthread-suppress</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> stays byte-identical to the original settled ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> is also byte-identical to the same hash</li>
      <li>full sampled-run concatenated PNG hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the late stable ACTIVITY ocean is not being sustained by the background thread</li>
      <li>neither late wave redraw nor late background animation is the repopulation path</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late background-thread suppression proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1941-pdt---late-ttmplay-suppression-also-does-not-move-the-settled-ocean">19:41 PDT - Late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> suppression also does not move the settled ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>, suppressed:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ttmPlay(&amp;ttmThreads[i])</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
      <li>left background-thread work intact</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-ttm-suppress</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> stays byte-identical to the original settled ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> is also identical</li>
      <li>early lead-in still differs:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash is unchanged from the background-thread proof:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>by the settled <code class="language-plaintext highlighter-rouge">04910+</code> failure band, the stable bad ocean no longer depends on late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> execution either</li>
      <li>so the image is surviving after both:
        <ul>
          <li>late background-thread work</li>
          <li>late scene-thread opcode playback</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> suppression proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1943-pdt---zeroing-all-background-tiles-immediately-before-loadimage-still-does-not-move-the-settled-ocean">19:43 PDT - Zeroing all background tiles immediately before <code class="language-plaintext highlighter-rouge">LoadImage()</code> still does not move the settled ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>zeroed all four <code class="language-plaintext highlighter-rouge">bgTile*-&gt;pixels</code></li>
          <li>forced their dirty ranges full-height</li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
      <li>this is the last shared upload boundary before the frame reaches VRAM</li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-upload-blackproof</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> still matches the exact original settled ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> also stays identical</li>
      <li>early lead-in still differs:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash remains:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean is surviving even past the normal tile-upload boundary</li>
      <li>it is not explained by:
        <ul>
          <li>late background-thread redraw</li>
          <li>late <code class="language-plaintext highlighter-rouge">ttmPlay()</code> scene-thread execution</li>
          <li>or even the current contents of the four background tile RAM surfaces at <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload time</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary upload-blackproof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move the proof surface to framebuffer/display ownership itself:
        <ul>
          <li>determine whether the visible settled ocean is coming from a different renderer surface/path than the tile upload path</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1947-pdt---direct-late-framebuffer-black-upload-does-change-the-settled-activity-image">19:47 PDT - Direct late framebuffer black upload does change the settled ACTIVITY image</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, immediately after <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> inside <code class="language-plaintext highlighter-rouge">grUpdateDisplay()</code>:
        <ul>
          <li>uploaded a static all-zero <code class="language-plaintext highlighter-rouge">320x240</code> tile into all four framebuffer quadrants</li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-framebuffer-blackproof</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the settled late ocean disappears</li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code>, <code class="language-plaintext highlighter-rouge">frame_04770</code>, and <code class="language-plaintext highlighter-rouge">frame_05000</code> all become:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash changes to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">8d305707d22f695b403e105f5ca11807281cfb1799cb6074b4a0e96889767e56</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the visible settled ACTIVITY image is absolutely on the normal displayed framebuffer</li>
      <li>so the earlier negative <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> tile-scrub proof does <strong>not</strong> mean “different renderer/output surface”</li>
      <li>instead, it means the settled ocean is surviving into the visible framebuffer in a way that was not affected by the earlier tile-RAM/upload scrubs</li>
      <li>the likely next target is now the exact interaction between:
        <ul>
          <li>dirty-row tracking / upload eligibility</li>
          <li>framebuffer persistence across frames</li>
          <li>any direct framebuffer writes outside the tile upload proof</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary framebuffer-blackproof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument why scrubbing the tile-upload source did not move the framebuffer, while a direct post-upload framebuffer write did</li>
    </ul>
  </li>
</ul>

<h2 id="1951-pdt---replacing-the-grdrawbackground-upload-source-with-black-does-change-the-settled-activity-image">19:51 PDT - Replacing the <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload source with black does change the settled ACTIVITY image</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>replaced the normal <code class="language-plaintext highlighter-rouge">LoadImage(..., tiles[i]-&gt;pixels + minY * w)</code> source with a static all-zero upload buffer</li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> after <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-upload-source-blackproof</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>the settled late ocean disappears</li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code>, <code class="language-plaintext highlighter-rouge">frame_04770</code>, and <code class="language-plaintext highlighter-rouge">frame_05000</code> all become:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash changes to:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">8d305707d22f695b403e105f5ca11807281cfb1799cb6074b4a0e96889767e56</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY image is still sourced through the normal <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> upload path</li>
      <li>so the earlier negative “tile memset + mark dirty” proof was not evidence of an alternate renderer/output surface</li>
      <li>it only means that forcing the tile RAM contents to zero in that earlier proof did not actually alter the uploaded source in the way expected</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary upload-source-blackproof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>debug why the earlier tile-RAM scrub failed while replacing the actual <code class="language-plaintext highlighter-rouge">LoadImage</code> source succeeded</li>
      <li>most likely surfaces now:
        <ul>
          <li>dirty-row/source-pointer semantics in <code class="language-plaintext highlighter-rouge">grDrawBackground()</code></li>
          <li>tile RAM contents vs. expected upload window</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1956-pdt---overwriting-the-exact-heap-upload-slice-still-does-not-move-the-settled-ocean">19:56 PDT - Overwriting the exact heap upload slice still does not move the settled ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, overwrote the exact heap slice
            <ul>
              <li><code class="language-plaintext highlighter-rouge">tiles[i]-&gt;pixels + minY * w</code></li>
            </ul>
          </li>
          <li>with zeroes immediately before the normal <code class="language-plaintext highlighter-rouge">LoadImage(...)</code> call</li>
        </ul>
      </li>
      <li>this kept:
        <ul>
          <li>the same rect</li>
          <li>the same source pointer</li>
          <li>the same upload path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-upload-slice-blackproof</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> still matches the original settled ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> also stays identical</li>
      <li>earlier lead-in still differs:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770 = 0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash remains the old one:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>replacing the <code class="language-plaintext highlighter-rouge">LoadImage</code> source pointer works</li>
      <li>but mutating the exact heap source slice in place does not</li>
      <li>so the remaining renderer bug is now strongly pointing at source-memory behavior, not upload-path selection</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary upload-slice-blackproof patch after validation</li>
    </ul>
  </li>
</ul>

<h2 id="2000-pdt---fresh-address-upload-only-perturbs-the-lead-in-not-the-settled-04910-ocean">20:00 PDT - Fresh-address upload only perturbs the lead-in, not the settled <code class="language-plaintext highlighter-rouge">04910</code> ocean</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> only when <code class="language-plaintext highlighter-rouge">grCurrentFrame</code> was in the tight <code class="language-plaintext highlighter-rouge">4909..4910</code> band</li>
          <li>replaced the normal source with a freshly allocated zeroed upload buffer</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-upload-freshaddr-4910</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> still matches the original settled ocean:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> also stays identical</li>
      <li>but the immediate pre-failure frame does change:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04900 = d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not clean enough to claim the “fresh unique source address fixes late uploads” theory</li>
      <li>but it does show the renderer is sensitive to source-buffer identity/timing right at the failure boundary</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary upload-freshaddr-4910 patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>keep the bug class as renderer-side <code class="language-plaintext highlighter-rouge">LoadImage</code> source-memory behavior</li>
      <li>but move the proof surface to a slightly wider <code class="language-plaintext highlighter-rouge">04900–04910</code> upload window, since the exact-frame gate is too narrow to be definitive</li>
    </ul>
  </li>
</ul>

<h2 id="2001-pdt---widened-fresh-address-upload-window-is-cleanly-negative">20:01 PDT - Widened fresh-address upload window is cleanly negative</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, replaced the normal upload source with a freshly allocated zeroed buffer</li>
          <li>across the full <code class="language-plaintext highlighter-rouge">grCurrentFrame</code> window <code class="language-plaintext highlighter-rouge">4900..4910</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-upload-freshaddr-4900-4910</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash also stays unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the simple “fresh unique source address in the failure band fixes the late renderer state” theory is now ruled out</li>
      <li>current renderer-side read is narrower:
        <ul>
          <li>replacing the upload source globally with a static black buffer works</li>
          <li>mutating the in-place heap slice does not</li>
          <li>swapping to fresh zeroed buffers only in the handoff band also does not</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the widened fresh-address proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>stop treating this as a generic source-address problem</li>
      <li>move to exact upload-window geometry / dirty-range behavior at <code class="language-plaintext highlighter-rouge">04910</code></li>
    </ul>
  </li>
</ul>

<h2 id="2005-pdt---forcing-full-height-uploads-across-0490004910-makes-the-settled-ocean-disappear">20:05 PDT - Forcing full-height uploads across <code class="language-plaintext highlighter-rouge">04900–04910</code> makes the settled ocean disappear</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> and only during <code class="language-plaintext highlighter-rouge">grCurrentFrame</code> <code class="language-plaintext highlighter-rouge">4900..4910</code></li>
          <li>forced every background tile upload window to full height:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">minY = 0</code></li>
              <li><code class="language-plaintext highlighter-rouge">maxY = tile-&gt;height - 1</code></li>
            </ul>
          </li>
          <li>and replaced the upload source with a static all-zero black buffer</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-force-dirty-black-4900-4910</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code> changes to black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> changes to black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> also stays black:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">0092914af727e5d656269735cf9d7cfffa0cb120f2966b57c23fa9e0fe1095e9</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">8d305707d22f695b403e105f5ca11807281cfb1799cb6074b4a0e96889767e56</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is the sharpest renderer cut so far</li>
      <li>the settled ACTIVITY ocean is still entirely controlled by <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> uploads</li>
      <li>and the key remaining bug class is now dirty-range/upload eligibility at the handoff window</li>
      <li>i.e. the normal upload window at <code class="language-plaintext highlighter-rouge">04910</code> is not covering or replacing the visible framebuffer the way it should</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary force-dirty-black proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect the exact <code class="language-plaintext highlighter-rouge">minY/maxY</code> dirty-row state leading into <code class="language-plaintext highlighter-rouge">04910</code></li>
      <li>determine why the normal dirty window is insufficient while forced full-height upload fixes the frame</li>
    </ul>
  </li>
</ul>

<h2 id="2010-pdt---forcing-full-height-uploads-with-the-normal-source-does-nothing">20:10 PDT - Forcing full-height uploads with the normal source does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grDrawBackground()</code>:
        <ul>
          <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> during <code class="language-plaintext highlighter-rouge">grCurrentFrame</code> <code class="language-plaintext highlighter-rouge">4900..4910</code></li>
          <li>forced:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">minY = 0</code></li>
              <li><code class="language-plaintext highlighter-rouge">maxY = tile-&gt;height - 1</code></li>
            </ul>
          </li>
          <li>but kept the normal upload source:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">tiles[i]-&gt;pixels + minY * w</code></li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li>direct frame capture under:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-force-dirty-normal-4900-4910</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">frame_04900</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">d9ee0d2e8f8465ea471c9c79223f1177c7225a238274f7f68ab32ed94312c0a0</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_04910</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">frame_05000</code> is unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
      <li>full sampled-run concatenated PNG hash also remains unchanged:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">31354944eee50c711b70eecc308bb70028cd04247471e2646de8b0ad45fa5002</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this cleanly rules out “dirty upload window is too small” as a sufficient explanation by itself</li>
      <li>widening the upload window alone does not move the settled ocean</li>
      <li>the current renderer-side read is now:
        <ul>
          <li>replacing the upload source with black works</li>
          <li>widening the upload window alone does not</li>
          <li>so the decisive remaining issue is the content of the normal upload source at the handoff, not just row eligibility</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary force-dirty-normal proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>inspect what is actually in the normal tile upload source at <code class="language-plaintext highlighter-rouge">04910</code></li>
      <li>and why it remains ocean while black-source substitution succeeds</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li>and exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending</li>
          <li>called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> instead of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fallback-bootscene-null/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is not being explained by the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare branch either</li>
      <li>so the remaining bad path is more specific than:
        <ul>
          <li>exact top-level playprepared branch</li>
          <li>exact post-adsPlay no-launch branch</li>
          <li>obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary fallback proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code>, called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-release-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not just island/background state persisting because <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> is too weak after prepare</li>
      <li>even explicit island release plus black-background init at that top-level handoff point does not move the result</li>
      <li>that strengthens the read that the stable bad-ocean path is bypassing or outliving the obvious top-level exact handoff hooks entirely</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grLoadScreen(char *strArg)</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
          <li>and <code class="language-plaintext highlighter-rouge">strArg</code> matches <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code></li>
          <li>then call <code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code> and return</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-late-ocean-screen-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean is not being actively re-established by a late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> call on the failure path</li>
      <li>that strengthens the read that the bad ocean is coming from an earlier background load that survives into the late window, not a later screen reload</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late-<code class="language-plaintext highlighter-rouge">grLoadScreen()</code> blackproof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash">19:26 PDT - Zeroing restored background tiles every frame still leaves the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code>:
        <ul>
          <li>after restoring clean copies</li>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
          <li>memset the active restored background tiles to black</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-restore-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is being repopulated after <code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code>, before upload</li>
      <li>so it is not merely a stale clean-background restore artifact surviving untouched through the late window</li>
      <li>this moves the remaining live suspect surface to late per-frame compositing/drawing, not background restore</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary restore-blackproof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1930-pdt---disabling-saved-rect-replay-still-leaves-the-exact-original-ocean-hash">19:30 PDT - Disabling saved-rect replay still leaves the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grApplySavedRects()</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
          <li>return immediately without replaying any saved rects</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-savedrect-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>saved-rect replay is not the repopulation path either</li>
      <li>the settled ocean is still being drawn later in the frame by live compositing/drawing after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code></li>
          <li>and after <code class="language-plaintext highlighter-rouge">grApplySavedRects()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary saved-rect blackproof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move directly into late live compositing between restore and upload:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">ttmPlay()</code> / background-thread draws / holiday-thread draws / sprite compositing</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-1">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-1">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-1">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-1">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li>and exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending</li>
          <li>called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> instead of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fallback-bootscene-null/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is not being explained by the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare branch either</li>
      <li>so the remaining bad path is more specific than:
        <ul>
          <li>exact top-level playprepared branch</li>
          <li>exact post-adsPlay no-launch branch</li>
          <li>obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary fallback proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-1">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code>, called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-release-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not just island/background state persisting because <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> is too weak after prepare</li>
      <li>even explicit island release plus black-background init at that top-level handoff point does not move the result</li>
      <li>that strengthens the read that the stable bad-ocean path is bypassing or outliving the obvious top-level exact handoff hooks entirely</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-1">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grLoadScreen(char *strArg)</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
          <li>and <code class="language-plaintext highlighter-rouge">strArg</code> matches <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code></li>
          <li>then call <code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code> and return</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-late-ocean-screen-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean is not being actively re-established by a late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> call on the failure path</li>
      <li>that strengthens the read that the bad ocean is coming from an earlier background load that survives into the late window, not a later screen reload</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late-<code class="language-plaintext highlighter-rouge">grLoadScreen()</code> blackproof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1926-pdt---zeroing-restored-background-tiles-every-frame-still-leaves-the-exact-original-ocean-hash-1">19:26 PDT - Zeroing restored background tiles every frame still leaves the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code>:
        <ul>
          <li>after restoring clean copies</li>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grCurrentFrame &gt;= 1000</code></li>
          <li>memset the active restored background tiles to black</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-restore-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is being repopulated after <code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code>, before upload</li>
      <li>so it is not merely a stale clean-background restore artifact surviving untouched through the late window</li>
      <li>this moves the remaining live suspect surface to late per-frame compositing/drawing, not background restore</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary restore-blackproof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>instrument or suppress late per-frame compositing paths between <code class="language-plaintext highlighter-rouge">grRestoreBgTiles()</code> and <code class="language-plaintext highlighter-rouge">grDrawBackground()</code> to identify what is repopulating the ocean after restore</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-2">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-2">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-2">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-2">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li>and exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending</li>
          <li>called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> instead of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fallback-bootscene-null/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is not being explained by the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare branch either</li>
      <li>so the remaining bad path is more specific than:
        <ul>
          <li>exact top-level playprepared branch</li>
          <li>exact post-adsPlay no-launch branch</li>
          <li>obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary fallback proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-2">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code>, called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-release-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not just island/background state persisting because <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> is too weak after prepare</li>
      <li>even explicit island release plus black-background init at that top-level handoff point does not move the result</li>
      <li>that strengthens the read that the stable bad-ocean path is bypassing or outliving the obvious top-level exact handoff hooks entirely</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1921-pdt---forcing-late-grloadscreenocean0scr-loads-to-black-also-does-nothing-2">19:21 PDT - Forcing late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> loads to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code>, inside <code class="language-plaintext highlighter-rouge">grLoadScreen(char *strArg)</code>:
        <ul>
          <li>if exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override is pending</li>
          <li>and <code class="language-plaintext highlighter-rouge">grGetCurrentFrame() &gt;= 1000</code></li>
          <li>and <code class="language-plaintext highlighter-rouge">strArg</code> matches <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code></li>
          <li>then call <code class="language-plaintext highlighter-rouge">grInitEmptyBackground()</code> and return</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-late-ocean-screen-blackproof/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled <code class="language-plaintext highlighter-rouge">04910+</code> ocean is not being actively re-established by a late <code class="language-plaintext highlighter-rouge">grLoadScreen(\"OCEAN0?.SCR\")</code> call on the failure path</li>
      <li>that strengthens the read that the bad ocean is coming from an earlier background load that survives into the late window, not a later screen reload</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary late-<code class="language-plaintext highlighter-rouge">grLoadScreen()</code> blackproof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move the proof surface to background persistence and framebuffer ownership itself, because both:
        <ul>
          <li>obvious story-level branches</li>
          <li>late renderer reload hypothesis</li>
        </ul>
      </li>
      <li>are now ruled out directly</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-3">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-3">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-3">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-3">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li>and exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending</li>
          <li>called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> instead of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fallback-bootscene-null/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is not being explained by the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare branch either</li>
      <li>so the remaining bad path is more specific than:
        <ul>
          <li>exact top-level playprepared branch</li>
          <li>exact post-adsPlay no-launch branch</li>
          <li>obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary fallback proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1916-pdt---even-adsreleaseisland-adsnoisland-before-exact-playback-leaves-the-original-ocean-hash-untouched-3">19:16 PDT - Even <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> before exact playback leaves the original ocean hash untouched</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code>, called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsReleaseIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-release-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>this is not just island/background state persisting because <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> is too weak after prepare</li>
      <li>even explicit island release plus black-background init at that top-level handoff point does not move the result</li>
      <li>that strengthens the read that the stable bad-ocean path is bypassing or outliving the obvious top-level exact handoff hooks entirely</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary <code class="language-plaintext highlighter-rouge">adsReleaseIsland(); adsNoIsland();</code> proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>switch from behavioral scrubs to a more direct runtime identity proof for the code path active at the <code class="language-plaintext highlighter-rouge">04910</code> settled-ocean window, because the obvious top-level hooks and fallback branches are now directly ruled out</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-4">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-4">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-4">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1910-pdt---forcing-the-obvious-bootscene--null-fallback-prepare-path-to-black-also-does-nothing-4">19:10 PDT - Forcing the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare path to black also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, before <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code>:
        <ul>
          <li>if <code class="language-plaintext highlighter-rouge">bootScene == NULL</code></li>
          <li>and exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> override was still pending</li>
          <li>called <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> instead of <code class="language-plaintext highlighter-rouge">storyPrepareSceneState(finalScene)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-fallback-bootscene-null/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ocean is not being explained by the obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare branch either</li>
      <li>so the remaining bad path is more specific than:
        <ul>
          <li>exact top-level playprepared branch</li>
          <li>exact post-adsPlay no-launch branch</li>
          <li>obvious <code class="language-plaintext highlighter-rouge">bootScene == NULL</code> fallback prepare</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary fallback proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>move to a more direct runtime identity proof surface for the actual branch executing at the <code class="language-plaintext highlighter-rouge">04910</code> settled-ocean window, because the obvious story-level branches are now mostly ruled out by direct behavioral proofs</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-5">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-5">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
</ul>

<h2 id="1905-pdt---scrubbing-to-black-immediately-before-storyplaypreparedscenefinalscene--also-does-nothing-5">19:05 PDT - Scrubbing to black immediately before <code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, ...)</code> also does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, for:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">bootScene != NULL</code></li>
          <li>exact <code class="language-plaintext highlighter-rouge">finalScene == ACTIVITY.ADS 1</code></li>
        </ul>
      </li>
      <li>called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
      <li>immediately before:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(finalScene, prevSpot, prevHdg)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-pre-playprepared-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the settled ACTIVITY ocean path is bypassing or outliving even the top-level exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code> handoff hook in <code class="language-plaintext highlighter-rouge">storyPlay()</code></li>
      <li>that makes the current failure shape more explicit:
        <ul>
          <li>exact ACTIVITY prepare has strong causal linkage to the ocean</li>
          <li>but the stable bad path is not behaving like the straightforward top-level exact-playback branch we expected</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>identify the alternate path or retry/re-entry path that preserves or re-establishes the prepared ocean while bypassing both:
        <ul>
          <li>exact pre-<code class="language-plaintext highlighter-rouge">storyPlayPreparedScene(...)</code></li>
          <li>exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch cleanup</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-6">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
</ul>

<h2 id="1900-pdt---scrubbing-to-black-immediately-after-exact-adsplayactivityads-1-no-launch-return-does-nothing-6">19:00 PDT - Scrubbing to black immediately after exact <code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return does nothing</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, immediately after:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPlay(scene-&gt;adsName, scene-&gt;adsTagNo)</code></li>
        </ul>
      </li>
      <li>for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code>, if:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">!ps1AdsLastPlayLaunched</code></li>
        </ul>
      </li>
      <li>then called:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsNoIsland()</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-post-adsplay-noisland/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the unchanged original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>the stable ACTIVITY ocean path is not being explained by the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch return hook</li>
      <li>either:
        <ul>
          <li>that exact playback hook is not reached on the stable failure path</li>
          <li>or the late ocean path is being re-established elsewhere after it</li>
        </ul>
      </li>
      <li>combined with the exact prepare proofs, this keeps the strongest remaining live suspect centered on the prepare/intro-to-scene handoff rather than the exact post-playback failure branch</li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary post-<code class="language-plaintext highlighter-rouge">adsPlay()</code> <code class="language-plaintext highlighter-rouge">adsNoIsland()</code> proof patch</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>identify the path that reaches the settled ocean after exact ACTIVITY prepare while bypassing the exact post-<code class="language-plaintext highlighter-rouge">adsPlay(ACTIVITY.ADS, 1)</code> no-launch cleanup hook</li>
    </ul>
  </li>
</ul>

<h2 id="1855-pdt---returning-from-islandinit-immediately-after-grloadscreen-reproduces-the-exact-original-ocean-hash-7">18:55 PDT - Returning from <code class="language-plaintext highlighter-rouge">islandInit()</code> immediately after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> reproduces the exact original ocean hash</h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/island.c</code>, for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> boot path:
        <ul>
          <li>kept the normal initial:
            <ul>
              <li><code class="language-plaintext highlighter-rouge">grLoadScreen("NIGHT.SCR")</code> or <code class="language-plaintext highlighter-rouge">grLoadScreen("OCEAN0?.SCR")</code></li>
            </ul>
          </li>
          <li>then returned immediately from <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
        </ul>
      </li>
      <li>this bypassed all later island composition:
        <ul>
          <li>raft</li>
          <li><code class="language-plaintext highlighter-rouge">BACKGRND.BMP</code></li>
          <li>clouds</li>
          <li>island sprites</li>
          <li>initial wave draws</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-island-loadonly/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with the exact original stable bad-ocean hash:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 59cd749fc0952552db0f9748ec7421df288292773be8ee82f008bfb3f2fbf6fc</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>later island composition is not required at all for the persistent ACTIVITY failure</li>
      <li>the stable bad ocean is already fully established by the initial <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code> path inside <code class="language-plaintext highlighter-rouge">islandInit()</code></li>
      <li>this collapses the remaining causal surface tightly onto:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li><code class="language-plaintext highlighter-rouge">islandInit()</code></li>
          <li>specifically the initial <code class="language-plaintext highlighter-rouge">OCEAN0?.SCR</code> / <code class="language-plaintext highlighter-rouge">NIGHT.SCR</code> background load and the state path that leads there</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary early return after <code class="language-plaintext highlighter-rouge">grLoadScreen(...)</code></li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>determine why exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code> is reaching that background-load-only island path and never replacing it with live ACTIVITY scene content</li>
    </ul>
  </li>
</ul>

<h2 id="1845-pdt---skipping-exact-scene-island-state-calculation-perturbs-the-lead-in-but-ocean-still-settles-by-04910-1">18:45 PDT - Skipping exact scene island-state calculation perturbs the lead-in, but ocean still settles by <code class="language-plaintext highlighter-rouge">04910</code></h2>

<ul>
  <li>Temporary proof:
    <ul>
      <li>in <code class="language-plaintext highlighter-rouge">repo:/story.c</code>, skipped:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">storyCalculateIslandFromScene(scene)</code></li>
        </ul>
      </li>
      <li>only for exact <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS 1</code></li>
      <li>left:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsPrimeSceneResources(...)</code></li>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>normal intro flow</li>
          <li>normal later playback path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Validation run:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">repo:/regtest-results/activity1-ads1-seed1-skip-scene-islandcalc/result.json</code></li>
    </ul>
  </li>
  <li>Result:
    <ul>
      <li>run exits cleanly with:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">state_hash = 81b56aa4b51befe5badf1bdfff881504bc469fc5a829e1924a706c359620cf03</code></li>
        </ul>
      </li>
      <li>sampled frames:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">frame_04770</code>: black</li>
          <li><code class="language-plaintext highlighter-rouge">frame_04910</code>: ocean</li>
          <li><code class="language-plaintext highlighter-rouge">frame_05000</code>: ocean</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Important conclusion:
    <ul>
      <li>scene-specific island-state calculation does affect the lead-in before the settled failure</li>
      <li>but it does not prevent the stable wrong-ocean state from being re-established by <code class="language-plaintext highlighter-rouge">04910</code></li>
      <li>so it is not sufficient to explain the persistent ACTIVITY failure on its own</li>
      <li>the strongest remaining causal surface is still:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">adsInitIsland()</code></li>
          <li>especially <code class="language-plaintext highlighter-rouge">islandInit()</code> and its initial background load/composition path</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Cleanup:
    <ul>
      <li>reverted the temporary skip-scene-islandcalc proof patch after validation</li>
    </ul>
  </li>
  <li>Next target:
    <ul>
      <li>isolate <code class="language-plaintext highlighter-rouge">adsInitIsland()</code> / <code class="language-plaintext highlighter-rouge">islandInit()</code> more directly, because that is now the tightest surviving source of the settled wrong-ocean outcome</li>
    </ul>
  </li>
</ul>
]]></content>
  </entry><entry>
    <title>PS1 Restore Rollout History</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/rollout-history/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/rollout-history/</id>
    <published>2026-03-21T00:00:00-04:00</published>
    <updated>2026-03-21T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Dated rollout chronology of the PS1 restore pilot, preserved verbatim from the research README.</summary>
    <content type="html"><![CDATA[<p>This file preserves the dated rollout chronology that used to live in the
top-level research README. It is historical context, not the active source
of truth.</p>

<h2 id="historical-rollout-chronology">Historical rollout chronology</h2>

<p>The remainder of this document is preserved as a dated implementation history.
Use <code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/CURRENT_STATUS_2026-03-21.md</code>
for the active rollout snapshot; older dated notes below are useful for design
rationale, but not as the current source of truth.</p>

<h3 id="2026-03-18-update">2026-03-18 update</h3>

<p>Phase 7 now has an offline extractor for dirty-region candidates:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/extract-dirty-region-templates.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/DIRTY_REGION_TEMPLATE_SCHEMA.md</code></li>
</ul>

<p>This produces per-pack template JSON from scene-pack manifests plus extracted
<code class="language-plaintext highlighter-rouge">TTM</code> bytecode. It is intentionally offline-only for now so the runtime stays on
the last known-good rendering path while we identify candidate scene families.</p>

<p>An initial <code class="language-plaintext highlighter-rouge">BUILDING.ADS</code> runtime <code class="language-plaintext highlighter-rouge">CLEAR_SCREEN</code> consumer was tested and then
backed out after later black-background regressions. The useful result is still
the offline artifact and candidate selection, but runtime restore policy remains
on the last known-good path until template use is tied to validated per-scene
state instead of a fixed family-level rect.</p>

<p>The original scene-level pilot picked from that process is now archived in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_candidate_report_2026-03-18.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_pilot_spec_2026-03-18.json</code></li>
</ul>

<p>At that stage, the active narrow targets were:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">STAND.ADS tags 1-3</code>, which has only one BMP, two TTM owners, and a
<code class="language-plaintext highlighter-rouge">352x140</code> restore envelope</li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY.ADS tag 1</code>, which reuses the same generated-contract path through the
<code class="language-plaintext highlighter-rouge">MEANWHIL.TTM</code>, <code class="language-plaintext highlighter-rouge">SJMSSGE.TTM</code>, <code class="language-plaintext highlighter-rouge">SJWORK.TTM</code>, and <code class="language-plaintext highlighter-rouge">THEEND.TTM</code> cluster</li>
  <li><code class="language-plaintext highlighter-rouge">WALKSTUF.ADS tag 2</code>, which exercises <code class="language-plaintext highlighter-rouge">MJJOG.TTM</code>, <code class="language-plaintext highlighter-rouge">MJRAFT.TTM</code>, and
<code class="language-plaintext highlighter-rouge">WOULDBE.TTM</code>, including a two-region clear/save contract in <code class="language-plaintext highlighter-rouge">WOULDBE.TTM</code></li>
</ul>

<p>Those pilots now have a generated C-side artifact:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/ps1_restore_pilots.h</code></li>
</ul>

<p>It is emitted from <code class="language-plaintext highlighter-rouge">repo:/scripts/generate-restore-pilots-header.py</code>
using the checked-in JSON pilot specs, so the runtime can consume a small table
of validated scene contracts instead of reaching back into research JSON by
hand.</p>

<p>The offline spec path is now batched too:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/build-restore-pilot-spec.py</code>
can emit one spec per recommended pilot into
<code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_pilot_specs_2026-03-19</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/generate-restore-pilots-header.py</code>
can now promote a filtered subset from that directory into a runtime header,
so offline pre-calculation can scale faster without turning every new spec
into a live runtime policy immediately</li>
</ul>

<p>That batching is now expanded to the full current ADS surface too:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_candidate_report_full_2026-03-19.json</code>
ranks one recommended restore candidate per ADS family</li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_pilot_specs_full_2026-03-19</code>
contains the first ten-family pre-calculated spec batch:
<code class="language-plaintext highlighter-rouge">STAND</code>, <code class="language-plaintext highlighter-rouge">JOHNNY</code>, <code class="language-plaintext highlighter-rouge">WALKSTUF</code>, <code class="language-plaintext highlighter-rouge">ACTIVITY</code>, <code class="language-plaintext highlighter-rouge">FISHING</code>, <code class="language-plaintext highlighter-rouge">BUILDING</code>,
<code class="language-plaintext highlighter-rouge">VISITOR</code>, <code class="language-plaintext highlighter-rouge">MARY</code>, <code class="language-plaintext highlighter-rouge">MISCGAG</code>, and <code class="language-plaintext highlighter-rouge">SUZY</code></li>
  <li>the header generator now tolerates incomplete per-TTM rect rows by disabling
those hook ids instead of crashing, which means research-grade candidates can
move through the batch pipeline before every TTM row is runtime-ready</li>
</ul>

<p>That same pipeline now emits scene-scoped output too:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_scene_specs_full_2026-03-19</code>
contains one restore spec per ranked scene, <code class="language-plaintext highlighter-rouge">63</code> total, so offline conversion
no longer waits on one-family-at-a-time promotion</li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/CURRENT_STATUS_2026-03-21.json</code>
is the active rollout snapshot for day-to-day work</li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/archive/2026-03-19-rollout-snapshot/restore_rollout_manifest_2026-03-19.json</code>
is the original grouped-rollout snapshot and is now archived for historical
comparison only</li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_scene_clusters_2026-03-19.json</code>
groups those <code class="language-plaintext highlighter-rouge">63</code> scene specs into <code class="language-plaintext highlighter-rouge">34</code> shared restore contracts, which is
the right unit for grouped runtime promotion</li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/restore_cluster_specs_2026-03-19</code>
lifts those <code class="language-plaintext highlighter-rouge">34</code> contracts into reusable cluster specs, so the next runtime
enablement step can promote a whole contract in one move</li>
</ul>

<p>That offline pipeline has now been tightened again: the dirty-region extractor
normalizes signed TTM rectangle origins and clamps them to visible scene bounds
before unioning. That removed bogus wrapped rects like <code class="language-plaintext highlighter-rouge">x=65534</code> /
<code class="language-plaintext highlighter-rouge">width=65551</code> from the <code class="language-plaintext highlighter-rouge">VISITOR</code> family and regenerated the full scene-spec and
cluster-spec artifact set from corrected geometry.</p>

<p>With the full queue emitted, live promotion is now split between:</p>

<ul>
  <li>live pilots that are actually reachable through the current harness path</li>
  <li>blocked pilots that remain offline-only until their story/bootstrap route is
reproducible</li>
</ul>

<p>The current verified rollout count is <code class="language-plaintext highlighter-rouge">25 / 63</code> scenes, with <code class="language-plaintext highlighter-rouge">26</code> scene tags
currently present in the live generated header. The active verified families
are <code class="language-plaintext highlighter-rouge">STAND</code>, <code class="language-plaintext highlighter-rouge">JOHNNY</code>, <code class="language-plaintext highlighter-rouge">WALKSTUF</code>, and <code class="language-plaintext highlighter-rouge">MISCGAG</code>; <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS tag 4</code> is the
current bring-up route and is intentionally excluded from the verified count
until its stale extra-frame artifact is removed.
That next grouped slice is now underway: the runtime header consumes the shared
<code class="language-plaintext highlighter-rouge">STAND</code> cluster contract for tags <code class="language-plaintext highlighter-rouge">1-12</code>, and bounded forced runs of
<code class="language-plaintext highlighter-rouge">STAND.ADS 4</code> and <code class="language-plaintext highlighter-rouge">STAND.ADS 12</code> both stayed visually good. This is the first
real contract-sized expansion beyond the original pilot tags.</p>

<p>The same grouped rollout now extends to <code class="language-plaintext highlighter-rouge">JOHNNY</code> too: the runtime header keeps
the original proven singleton for <code class="language-plaintext highlighter-rouge">JOHNNY.ADS 1</code>, adds the shared contract for
<code class="language-plaintext highlighter-rouge">JOHNNY.ADS 2-5</code>, and now also carries the validated <code class="language-plaintext highlighter-rouge">JOHNNY.ADS 6</code> singleton
contract. Bounded forced runs of <code class="language-plaintext highlighter-rouge">JOHNNY.ADS 2</code>, <code class="language-plaintext highlighter-rouge">JOHNNY.ADS 5</code>, and
<code class="language-plaintext highlighter-rouge">JOHNNY.ADS 6</code> all stayed visually good, so this grouped rollout now covers
the full <code class="language-plaintext highlighter-rouge">JOHNNY.ADS 1-6</code> surface.</p>

<p><code class="language-plaintext highlighter-rouge">STAND</code> has also moved beyond the first cluster. In addition to the shared
<code class="language-plaintext highlighter-rouge">STAND.ADS 1-12</code> contract, the runtime header now carries the second shared
contract for <code class="language-plaintext highlighter-rouge">STAND.ADS 15-16</code>, and bounded forced runs of <code class="language-plaintext highlighter-rouge">STAND.ADS 15</code> and
<code class="language-plaintext highlighter-rouge">STAND.ADS 16</code> both stayed visually good. That makes <code class="language-plaintext highlighter-rouge">STAND.ADS 1-12</code> plus
<code class="language-plaintext highlighter-rouge">15-16</code> the current largest live grouped rollout.</p>

<p><code class="language-plaintext highlighter-rouge">BUILDING</code> was tried as the next grouped target, but both <code class="language-plaintext highlighter-rouge">island ads</code> and
<code class="language-plaintext highlighter-rouge">story ads</code> entry paths still land on bootstrap/ocean states instead of a valid
composed scene. That family stays offline-only for now; the blocker is entry
reproduction, not the generated restore contract itself.</p>

<p><code class="language-plaintext highlighter-rouge">WALKSTUF</code> has now been widened to cover tags <code class="language-plaintext highlighter-rouge">1-3</code> in the live generated
header. Forced runs of <code class="language-plaintext highlighter-rouge">WALKSTUF.ADS 1</code> and <code class="language-plaintext highlighter-rouge">WALKSTUF.ADS 3</code> stayed visually
good, so the scene-scoped restore contracts are viable there too, but both
routes still report active-pack fallbacks. That makes <code class="language-plaintext highlighter-rouge">WALKSTUF</code> the next
cleanup target: the restore policy is holding, while the remaining debt is in
pack completeness/runtime reads rather than scene composition.</p>

<p>That next slice is now in place in narrowly-scoped form: <code class="language-plaintext highlighter-rouge">ttm.c</code> has a
scene-scoped <code class="language-plaintext highlighter-rouge">CLEAR_SCREEN</code> pilot hook for the <code class="language-plaintext highlighter-rouge">STAND.ADS tags 1-3</code> pilot
cluster that only applies to <code class="language-plaintext highlighter-rouge">MJAMBWLK.TTM</code> and <code class="language-plaintext highlighter-rouge">MJTELE.TTM</code> using the
generated header rects, instead of another family-wide restore hook.</p>

<p>The pilot now also tracks <code class="language-plaintext highlighter-rouge">TTM_UNKNOWN_1</code> region ids in thread state and uses
the same generated contract for <code class="language-plaintext highlighter-rouge">SAVE_IMAGE1</code> on the narrow <code class="language-plaintext highlighter-rouge">STAND.ADS</code> pilot
cluster (<code class="language-plaintext highlighter-rouge">tags 1-3</code>).</p>

<p>The validation harness has been tightened around real PS1 boot timing too:
forced <code class="language-plaintext highlighter-rouge">STAND</code> boots now capture a short late-frame series rather than trying to
judge the route from early title-screen frames. Under that later capture window,
the <code class="language-plaintext highlighter-rouge">STAND</code> path comes up reliably and the decoder reports
<code class="language-plaintext highlighter-rouge">pilot_pack ... fallbacks=0</code>.</p>

<p>With that harness in place, the next Phase 7 cut is also live: on the same
<code class="language-plaintext highlighter-rouge">STAND.ADS tags 1-3</code> route, <code class="language-plaintext highlighter-rouge">ads.c</code> no longer uses replay merge, actor
recovery, or handoff carry/injection as a correctness mechanism. A fresh forced
<code class="language-plaintext highlighter-rouge">STAND.ADS 1</code> run still held visually with pack hits and zero fallbacks, so
this is the first route where the scene-scoped restore contract is beginning to
replace the older replay-resurrection model instead of merely coexisting with
it.</p>

<p>That same replay-policy cut now also goes through the <code class="language-plaintext highlighter-rouge">JOHNNY.ADS tag 1</code>
pilot via the shared generated pilot table. A fresh forced
<code class="language-plaintext highlighter-rouge">JOHNNY.ADS 1</code> run still held with <code class="language-plaintext highlighter-rouge">pilot_pack ... fallbacks=0</code>.</p>

<p>The normal boot path also flushed out a real baseline transition regression.
The first island handoff was fading to black and then collapsing instead of
completing the takeover into the next scene. The narrow fix was in
<code class="language-plaintext highlighter-rouge">repo:/ads.c</code>: <code class="language-plaintext highlighter-rouge">adsPlayWalk()</code> now resets
<code class="language-plaintext highlighter-rouge">ttmSlots[0]</code> after <code class="language-plaintext highlighter-rouge">adsStopScene(0)</code>, so stale <code class="language-plaintext highlighter-rouge">JOHNWALK</code> slot state no
longer leaks into the next ADS launch. Longer normal-boot capture runs now stay
alive across that first handoff instead of dropping to a permanent black frame.</p>

<p>One important validation detail from that same run: <code class="language-plaintext highlighter-rouge">story phase=4</code> persisting
for several captures is expected while the handoff is still inside
<code class="language-plaintext highlighter-rouge">adsPlayWalk()</code>. <code class="language-plaintext highlighter-rouge">storyPlay()</code> only advances to phase <code class="language-plaintext highlighter-rouge">5</code> after the walk path
returns, and the walk code intentionally holds an arrival pose before it exits.
So the remaining long <code class="language-plaintext highlighter-rouge">phase=4</code> window is not by itself evidence of another
stuck story-state bug.</p>

<p>One useful validation note from that route: the black-backed clock in the
<code class="language-plaintext highlighter-rouge">MEANWHIL</code> sequence is not a new restore regression. The original
<code class="language-plaintext highlighter-rouge">MEANWHIL.TTM</code> script explicitly issues <code class="language-plaintext highlighter-rouge">DRAW_RECT 0 0 640 350</code> after
<code class="language-plaintext highlighter-rouge">SET_COLORS 5 5</code>, then repeatedly draws the clock backing sprite. So that card
is authored scene behavior, not a pack-path failure.</p>

<p>The next generated pilot now exists too:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_pilot_spec_walkstuf_2026-03-18.json</code></li>
</ul>

<p>That keeps the runtime work on the same rails: expand the generated pilot table
one scene-scoped contract at a time instead of adding another family-wide
special case.</p>

<p>The pilot specs now carry explicit scene resource lists too, and the PS1
runtime primes those scene-scoped resources before play through
<code class="language-plaintext highlighter-rouge">repo:/ps1_restore_pilots.h</code>
and <code class="language-plaintext highlighter-rouge">repo:/ads.c</code>. That is the first real
offline-to-runtime link beyond dirty-rect policy: the generated spec now drives
which BMP/SCR/TTM assets get warmed for a pilot route.</p>

<p>One current validation note: the pack fallback telemetry is now wired to real
extracted-file reads instead of stale counters. <code class="language-plaintext highlighter-rouge">STAND.ADS</code> still validates with
<code class="language-plaintext highlighter-rouge">pilot_pack ... fallbacks=0</code>, and the <code class="language-plaintext highlighter-rouge">WALKSTUF.ADS 2</code> first-frame actor gap is
now fixed by re-enabling only one-frame missing-actor recovery on that pilot
route. The opening frame at
<code class="language-plaintext highlighter-rouge">ps1-test-20260319-083913.png</code> now shows Johnny holding the board where the
earlier <code class="language-plaintext highlighter-rouge">ps1-test-20260319-083316.png</code> run showed only the board. <code class="language-plaintext highlighter-rouge">WALKSTUF</code>
still reports one real active-pack miss (<code class="language-plaintext highlighter-rouge">WOULDBE.BMP</code> signature <code class="language-plaintext highlighter-rouge">17</code>) during
the bounded harness route, but that remaining miss is now a secondary cleanup,
not the visible scene-entry regression.</p>

<details class="page-toc">
  <summary>On this page</summary>

<ul id="markdown-toc">
  <li><a href="#historical-rollout-chronology" id="markdown-toc-historical-rollout-chronology">Historical rollout chronology</a>    <ul>
      <li><a href="#2026-03-18-update" id="markdown-toc-2026-03-18-update">2026-03-18 update</a></li>
    </ul>
  </li>
  <li><a href="#current-facts-from-the-repo" id="markdown-toc-current-facts-from-the-repo">Current facts from the repo</a>    <ul>
      <li><a href="#1-the-pc-and-ps1-rendering-models-do-not-match" id="markdown-toc-1-the-pc-and-ps1-rendering-models-do-not-match">1. The PC and PS1 rendering models do not match</a></li>
      <li><a href="#2-the-project-already-uses-iso-space-to-trade-for-ram-but-inconsistently" id="markdown-toc-2-the-project-already-uses-iso-space-to-trade-for-ram-but-inconsistently">2. The project already uses ISO space to trade for RAM, but inconsistently</a></li>
      <li><a href="#3-static-analysis-is-already-strong-enough-to-drive-compilation-decisions" id="markdown-toc-3-static-analysis-is-already-strong-enough-to-drive-compilation-decisions">3. Static analysis is already strong enough to drive compilation decisions</a></li>
      <li><a href="#4-analyzer-v2-output-is-now-machine-readable" id="markdown-toc-4-analyzer-v2-output-is-now-machine-readable">4. Analyzer v2 output is now machine-readable</a></li>
      <li><a href="#5-pack-planner-consumer-exists" id="markdown-toc-5-pack-planner-consumer-exists">5. Pack planner consumer exists</a></li>
      <li><a href="#6-scene-pack-compiler-and-generic-loader-exist" id="markdown-toc-6-scene-pack-compiler-and-generic-loader-exist">6. Scene pack compiler and generic loader exist</a></li>
      <li><a href="#7-transition--prefetch-post-processing" id="markdown-toc-7-transition--prefetch-post-processing">7. Transition / prefetch post-processing</a></li>
      <li><a href="#8-dirty-region-and-restore-runtime" id="markdown-toc-8-dirty-region-and-restore-runtime">8. Dirty-region and restore runtime</a></li>
      <li><a href="#9-sdl-compat-lite-contract" id="markdown-toc-9-sdl-compat-lite-contract">9. SDL-Compat Lite contract</a></li>
    </ul>
  </li>
  <li><a href="#recommended-architecture" id="markdown-toc-recommended-architecture">Recommended architecture</a>    <ul>
      <li><a href="#recommendation-a-compiled-scene-packs" id="markdown-toc-recommendation-a-compiled-scene-packs">Recommendation A: Compiled scene packs</a></li>
      <li><a href="#recommendation-b-offline-sprite-transcoding-to-ps1-native-blit-format" id="markdown-toc-recommendation-b-offline-sprite-transcoding-to-ps1-native-blit-format">Recommendation B: Offline sprite transcoding to PS1-native blit format</a></li>
      <li><a href="#recommendation-c-sdl-compat-lite-runtime-contract" id="markdown-toc-recommendation-c-sdl-compat-lite-runtime-contract">Recommendation C: SDL-Compat Lite runtime contract</a></li>
      <li><a href="#recommendation-d-scene-local-frame-cache-with-static-prefetch" id="markdown-toc-recommendation-d-scene-local-frame-cache-with-static-prefetch">Recommendation D: Scene-local frame cache with static prefetch</a></li>
    </ul>
  </li>
  <li><a href="#what-should-move-offline" id="markdown-toc-what-should-move-offline">What should move offline</a></li>
  <li><a href="#what-should-stay-runtime" id="markdown-toc-what-should-stay-runtime">What should stay runtime</a></li>
  <li><a href="#research-conclusions" id="markdown-toc-research-conclusions">Research conclusions</a>    <ul>
      <li><a href="#best-target-state" id="markdown-toc-best-target-state">Best target state</a></li>
      <li><a href="#best-incremental-path" id="markdown-toc-best-incremental-path">Best incremental path</a></li>
      <li><a href="#what-not-to-do" id="markdown-toc-what-not-to-do">What not to do</a></li>
    </ul>
  </li>
  <li><a href="#external-references" id="markdown-toc-external-references">External references</a></li>
  <li><a href="#related-files-in-this-research-package" id="markdown-toc-related-files-in-this-research-package">Related files in this research package</a></li>
</ul>

</details>

<h2 id="current-facts-from-the-repo">Current facts from the repo</h2>

<h3 id="1-the-pc-and-ps1-rendering-models-do-not-match">1. The PC and PS1 rendering models do not match</h3>

<p>PC path:</p>

<ul>
  <li>Composes <code class="language-plaintext highlighter-rouge">background + savedZones + thread layers + holiday layer</code>.</li>
  <li>Uses persistent per-thread <code class="language-plaintext highlighter-rouge">ttmLayer</code> surfaces.</li>
  <li>Relies on direct sprite blits plus save/restore zone behavior.</li>
</ul>

<p>Key files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/graphics.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/graphics.h</code></li>
</ul>

<p>PS1 path:</p>

<ul>
  <li>Restores clean background tiles, composites indexed sprites into RAM tiles, then
uploads those tiles every frame.</li>
  <li>Keeps replay records in thread state and rebuilds visual continuity from those
records.</li>
  <li>Has partial or empty implementations for several SDL-like primitives and
save/restore calls.</li>
</ul>

<p>Key files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.h</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/ads.c</code></li>
</ul>

<h3 id="2-the-project-already-uses-iso-space-to-trade-for-ram-but-inconsistently">2. The project already uses ISO space to trade for RAM, but inconsistently</h3>

<p>The PS1 port already bypasses resource-file decompression for hot assets and loads
pre-extracted files from disc:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BMP/</code></li>
  <li><code class="language-plaintext highlighter-rouge">SCR/</code></li>
  <li><code class="language-plaintext highlighter-rouge">TTM/</code></li>
  <li><code class="language-plaintext highlighter-rouge">ADS/</code></li>
</ul>

<p>Key file:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
</ul>

<p>That is already the right general instinct. The issue is that it stops at
“pre-extracted original assets” rather than going all the way to “PS1-runtime-native
compiled assets.”</p>

<h3 id="3-static-analysis-is-already-strong-enough-to-drive-compilation-decisions">3. Static analysis is already strong enough to drive compilation decisions</h3>

<p>The scene analyzer already computes:</p>

<ul>
  <li>per-scene BMP/SCR/TTM/ADS usage</li>
  <li>estimated peak memory</li>
  <li>sprite frame counts</li>
  <li>concurrent thread counts</li>
  <li>global heavy-scene rankings</li>
  <li>machine-readable JSON for build-time tooling</li>
  <li>derived heuristics for scene clustering, shared resources, transition churn, and
ADS-family prefetch candidates</li>
</ul>

<p>Key files:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scene_analyzer.c</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/analyze-scenes.sh</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/Makefile.analyzer</code></li>
</ul>

<p>This means the next step is not “invent analysis from scratch.” The next step is
“extend the analyzer so it emits the data the build pipeline needs.”</p>

<h3 id="4-analyzer-v2-output-is-now-machine-readable">4. Analyzer v2 output is now machine-readable</h3>

<p><code class="language-plaintext highlighter-rouge">scene_analyzer</code> now supports <code class="language-plaintext highlighter-rouge">--json</code> in addition to the original text report.</p>

<p>Command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/analyze-scenes.sh <span class="nt">--json</span> <span class="o">&gt;</span> docs/ps1/research/generated/scene_analysis_output_2026-03-17.json
</code></pre></div></div>

<p>Current schema highlights:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">summary</code>
    <ul>
      <li>global heaviest-scene and concurrency maxima</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">derived.candidate_scene_clusters</code>
    <ul>
      <li>current heuristic groups scenes by ADS file as a pack-compilation starting point</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">derived.shared_resources</code>
    <ul>
      <li>BMP/TTM inventories shared across multiple scenes</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">derived.heaviest_transition_deltas</code>
    <ul>
      <li>highest-churn sequential scene-to-scene deltas for pack-boundary review</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">derived.likely_prefetch_sets</code>
    <ul>
      <li>ADS-family union heuristic for first-pass prefetch planning</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">scenes[*]</code>
    <ul>
      <li>story metadata, resource bindings, thread launches, and explicit memory
components</li>
    </ul>
  </li>
</ul>

<p>Important caveat:</p>

<ul>
  <li>transition and prefetch outputs are currently heuristic rather than proven runtime
transition graphs</li>
  <li>pointer-table accounting is now explicitly PS1-sized at <code class="language-plaintext highlighter-rouge">4</code> bytes per pointer,
rather than using host <code class="language-plaintext highlighter-rouge">sizeof(void *)</code></li>
</ul>

<h3 id="5-pack-planner-consumer-exists">5. Pack planner consumer exists</h3>

<p>The first build-facing consumer of the analyzer JSON is now checked in:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/plan-scene-packs.py <span class="se">\</span>
  <span class="nt">--output</span> docs/ps1/research/generated/scene_pack_plan_2026-03-17.json <span class="se">\</span>
  <span class="nt">--manifest-dir</span> docs/ps1/research/generated/scene_pack_manifests_2026-03-17
</code></pre></div></div>

<p>What it emits:</p>

<ul>
  <li>one aggregate plan file for the pack compiler</li>
  <li>one manifest per ADS-family pack</li>
  <li>per-pack resource aggregates with global and pack-local reference counts</li>
  <li>transition-driven prefetch candidates, with ADS-family fallback still available</li>
</ul>

<p>Important caveat:</p>

<ul>
  <li>this is a planning consumer, not a runtime loader</li>
  <li>the pack IDs and prefetch links are heuristic, derived from the analyzer JSON</li>
  <li>the manifests are intentionally shaped to make the later compiler a mechanical
step rather than a discovery step</li>
</ul>

<h3 id="6-scene-pack-compiler-and-generic-loader-exist">6. Scene pack compiler and generic loader exist</h3>

<p>The compiler now consumes either one manifest or the full manifest directory and
emits concrete compiled packs for every current ADS family:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/compile-scene-pack.py <span class="nt">--all</span>
</code></pre></div></div>

<p>Current outputs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/compile-scene-pack.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/PACK_PAYLOAD_LAYOUT.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/compiled_packs_2026-03-17</code></li>
</ul>

<p>What it emits:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">pack_payload.bin</code>
    <ul>
      <li>deterministic raw resource blob</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">pack_index.json</code>
    <ul>
      <li>sector-aligned offsets, sizes, checksums, and runtime envelope metadata</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">jc_resources/packs/*.PAK</code>
    <ul>
      <li>staged CD-visible payloads for all current ADS families, each with a compact
binary TOC at the front of the file</li>
    </ul>
  </li>
</ul>

<p>Current constraints:</p>

<ul>
  <li>resource order is fixed as <code class="language-plaintext highlighter-rouge">ADS -&gt; SCR -&gt; TTM -&gt; BMP</code></li>
  <li>each resource is aligned to <code class="language-plaintext highlighter-rouge">2048</code> bytes</li>
  <li>this is a loader target and format draft, but the runtime now consumes the
binary TOC embedded in each pack rather than generated C tables</li>
</ul>

<p>Runtime hook:</p>

<ul>
  <li>ADS loads now activate a pack-first lookup path in
<code class="language-plaintext highlighter-rouge">repo:/cdrom_ps1.c</code></li>
  <li>all current <code class="language-plaintext highlighter-rouge">ACTIVITY/BUILDING/FISHING/JOHNNY/MARY/MISCGAG/STAND/SUZY/VISITOR/WALKSTUF</code>
families now try their staged pack payload first</li>
  <li>once an ADS-family pack is active, <code class="language-plaintext highlighter-rouge">ADS/SCR/TTM/BMP</code> payloads are all
pack-authoritative and no longer fall back to the extracted-file path</li>
  <li>bounded DuckStation validation now decodes <code class="language-plaintext highlighter-rouge">pilot_pack active_pack_id=7 hits=7
fallbacks=0</code> on the working scene path, so the pack path is active without
observed extracted-asset fallback in that traversal</li>
  <li>the generated lookup table is now shared rather than BUILDING-specific</li>
</ul>

<h3 id="7-transition--prefetch-post-processing">7. Transition / prefetch post-processing</h3>

<p>The analyzer JSON is now fed through a small post-processor that turns the raw
scene sequence into more actionable planning output:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/scripts/scene-transition-prefetch-report.py</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/TRANSITION_PREFETCH_SCHEMA.md</code></li>
</ul>

<p>This post-processor adds:</p>

<ul>
  <li>pack candidates with unioned resource bytes</li>
  <li>adjacent transition edges with added/shared/removed byte counts</li>
  <li>ranked prefetch edges based on added working set</li>
</ul>

<h3 id="8-dirty-region-and-restore-runtime">8. Dirty-region and restore runtime</h3>

<p>The PS1 runtime now has its first explicit region-restore implementation instead
of relying entirely on whole-frame background restore plus replay continuity:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/graphics_ps1.c</code> now implements
<code class="language-plaintext highlighter-rouge">grSaveZone()</code> / <code class="language-plaintext highlighter-rouge">grRestoreZone()</code> against the clean background tile copies</li>
  <li>the implementation tracks one active saved zone, matching the existing PC-side
assumption for Johnny’s TTMs</li>
  <li><code class="language-plaintext highlighter-rouge">RESTORE_ZONE</code> can now restore a bounded rectangle from the pristine background
tiles during TTM playback rather than acting as a no-op on PS1</li>
  <li>bounded DuckStation validation still boots the working scene and decodes
<code class="language-plaintext highlighter-rouge">pilot_pack ... fallbacks=0</code> after this change</li>
</ul>

<h3 id="9-sdl-compat-lite-contract">9. SDL-Compat Lite contract</h3>

<p>The first written contract for the narrow runtime boundary now lives in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/SDL_COMPAT_LITE_SPEC.md</code></li>
</ul>

<p>It captures:</p>

<ul>
  <li>the minimum gameplay-facing graphics surface</li>
  <li>the current PC/PS1 gap matrix</li>
  <li>the places where PS1 still leaks replay-era implementation details into
gameplay-visible correctness</li>
  <li>ranked pack-boundary candidates for disc-layout review</li>
</ul>

<p>The caveat remains the same: these are story-order planning heuristics, not a
validated runtime transition graph.</p>

<h2 id="recommended-architecture">Recommended architecture</h2>

<h3 id="recommendation-a-compiled-scene-packs">Recommendation A: Compiled scene packs</h3>

<p>This is the highest-leverage option.</p>

<p>Instead of loading generic BMP/SCR/TTM/ADS assets at runtime and then dynamically
figuring out what must stay live, build one compiled pack per ADS tag or tag-cluster.</p>

<p>Each pack should contain:</p>

<ul>
  <li>resolved TTM set</li>
  <li>scene-local sprite banks</li>
  <li>pretranscoded sprite frame data</li>
  <li>precomputed metadata for thread maxima</li>
  <li>precomputed memory envelope for enter / steady / exit phases</li>
  <li>optional prefetch links to likely next packs</li>
</ul>

<p>Runtime effect:</p>

<ul>
  <li>less generic resource management</li>
  <li>fewer live asset formats</li>
  <li>deterministic memory behavior</li>
  <li>less reason for replay heuristics</li>
</ul>

<p>Tradeoff:</p>

<ul>
  <li>more ISO use</li>
  <li>more offline tooling</li>
  <li>need to choose pack granularity carefully</li>
</ul>

<h3 id="recommendation-b-offline-sprite-transcoding-to-ps1-native-blit-format">Recommendation B: Offline sprite transcoding to PS1-native blit format</h3>

<p>Convert every sprite frame offline into a format optimized for the actual PS1 draw
path, not for the original file format.</p>

<p>Strong candidates:</p>

<ul>
  <li>opaque span tables per scanline</li>
  <li>simple RLE by span</li>
  <li>pre-flipped spans for horizontal mirroring</li>
  <li>nibble order fixed ahead of time</li>
  <li>tile split decided offline rather than at load time</li>
</ul>

<p>Why this matters:</p>

<ul>
  <li>runtime sprite draw becomes a deterministic span blitter</li>
  <li>fewer per-pixel branches</li>
  <li>fewer chances to disagree with PC semantics</li>
  <li>removes load-time format quirks from gameplay execution</li>
</ul>

<p>This is a better use of ISO space than repeatedly paying for runtime interpretation.</p>

<h3 id="recommendation-c-sdl-compat-lite-runtime-contract">Recommendation C: SDL-Compat Lite runtime contract</h3>

<p>Define a narrow runtime API that matches what the engine actually needs.</p>

<p>Minimum contract:</p>

<ul>
  <li>acquire/release logical layer</li>
  <li>begin frame / present frame</li>
  <li>draw sprite / draw flipped sprite</li>
  <li>draw rect / line / pixel</li>
  <li>set clip zone</li>
  <li>copy/save/restore zone</li>
</ul>

<p>Behavioral guarantees:</p>

<ul>
  <li>per-thread layer persistence</li>
  <li>transparent blit semantics</li>
  <li>deterministic present order</li>
  <li>working save/restore for at least the current script patterns</li>
</ul>

<p>The main design rule is:</p>

<p>Do not let ADS/TTM gameplay logic know about replay records, actor recovery, or
background-tile juggling.</p>

<h3 id="recommendation-d-scene-local-frame-cache-with-static-prefetch">Recommendation D: Scene-local frame cache with static prefetch</h3>

<p>If full compiled scene packs are too large as a first step, the fallback direction is
a bounded frame cache driven by static scene knowledge.</p>

<p>That means:</p>

<ul>
  <li>keep only scene-local active frames in RAM</li>
  <li>prefetch likely next frames or next scene pack</li>
  <li>evict by deterministic schedule, not general LRU guesswork</li>
</ul>

<p>This is weaker than compiled packs, but still much better than “runtime discovers
everything dynamically.”</p>

<h2 id="what-should-move-offline">What should move offline</h2>

<p>These are the best candidates for static or build-time computation:</p>

<ol>
  <li>Sprite frame transcoding
    <ul>
      <li>Convert BMP sprite sheets into PS1-native draw records.</li>
    </ul>
  </li>
  <li>Flip variants
    <ul>
      <li>Precompute flipped versions when that reduces runtime branches and edge cases.</li>
    </ul>
  </li>
  <li>Tile splitting
    <ul>
      <li>Decide how frames are split across 64px texture constraints offline.</li>
    </ul>
  </li>
  <li>Dirty-region templates
    <ul>
      <li>Precompute common dirty rectangles for stable sequences like walking, fire, fish,
coconut, and note scenes.</li>
    </ul>
  </li>
  <li>Scene memory envelopes
    <ul>
      <li>Emit per-scene “must reside” and “can stream” sets.</li>
    </ul>
  </li>
  <li>Transition handoff data
    <ul>
      <li>Emit exact persistence requirements at walk-to-scene and scene-to-scene boundaries.</li>
    </ul>
  </li>
  <li>CD layout hints
    <ul>
      <li>Group pack files physically to reduce seek penalties.</li>
    </ul>
  </li>
</ol>

<h2 id="what-should-stay-runtime">What should stay runtime</h2>

<p>Runtime should be limited to:</p>

<ul>
  <li>running ADS and TTM logic</li>
  <li>opening the correct compiled pack</li>
  <li>drawing from a deterministic surface API</li>
  <li>small bounded frame caching if needed</li>
  <li>presenting layers in a fixed order</li>
</ul>

<p>Runtime should not keep growing new correctness heuristics for:</p>

<ul>
  <li>actor continuity</li>
  <li>replay recovery</li>
  <li>draw-record resurrection</li>
  <li>dynamic sprite identity matching</li>
</ul>

<p>Those are symptoms of the wrong boundary.</p>

<h2 id="research-conclusions">Research conclusions</h2>

<h3 id="best-target-state">Best target state</h3>

<p>The strongest long-term target is:</p>

<p><code class="language-plaintext highlighter-rouge">Compiled scene packs + offline-transcoded sprites + SDL-Compat Lite runtime</code></p>

<p>That combination gives the biggest simplification win and aligns with the fact that
Johnny Castaway content is fixed, finite, and highly analyzable.</p>

<h3 id="best-incremental-path">Best incremental path</h3>

<p>If the team wants lower risk, use this order:</p>

<ol>
  <li>Extend analyzer to emit machine-readable scene data.</li>
  <li>Build an offline sprite transcoder for one problematic content family.</li>
  <li>Implement SDL-Compat Lite boundary.</li>
  <li>Route one scene family through compiled packs.</li>
  <li>Expand scene-pack coverage once correctness is proven.</li>
</ol>

<h3 id="what-not-to-do">What not to do</h3>

<p>Avoid investing heavily in deeper versions of the current replay-based model.</p>

<p>It may still be useful for short-term bug fixing, but it is not a good final
architecture. It leaks PS1-specific failure modes into gameplay behavior.</p>

<h2 id="external-references">External references</h2>

<p>These are useful for the build/runtime tradeoff discussion:</p>

<ul>
  <li>PSX-SPX CD-ROM drive notes:
https://psx-spx.consoledev.net/cdromdrive/</li>
  <li>PSX-SPX MDEC reference:
https://psx-spx.consoledev.net/macroblockdecodermdec/</li>
  <li>PSn00bSDK texture / CLUT tutorial mirror:
https://www.breck-mckye.com/psnoobsdk-docs/chapter1/3-textures.html</li>
</ul>

<p>Why they matter:</p>

<ul>
  <li>CD reads are fast enough to support deliberate streaming, but need disciplined
buffering and sector handling.</li>
  <li>4-bit indexed textures and CLUT constraints are native strengths of the PS1.</li>
  <li>MDEC exists, but it is a specialized JPEG-style path and is not automatically the
right fit for general sprite animation assets.</li>
</ul>

<h2 id="related-files-in-this-research-package">Related files in this research package</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/BACKLOG.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/IMPLEMENTATION_PLAN.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/PACK_MANIFEST_SCHEMA.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/PACK_PAYLOAD_LAYOUT.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_analysis_output_2026-03-17.txt</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_analysis_output_2026-03-17.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_pack_plan_2026-03-17.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_pack_manifests_2026-03-17</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/compiled_packs_2026-03-17</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.json</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.md</code></li>
  <li><code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/TRANSITION_PREFETCH_SCHEMA.md</code></li>
</ul>
]]></content>
  </entry><entry>
    <title>Current PS1 Restore Rollout Status — March 21, 2026</title>
    <link rel="alternate" type="text/html" href="https://hunterdavis.com/johnny-castaway-ps1/devlog/current-status-2026-03-21/"/>
    <id>https://hunterdavis.com/johnny-castaway-ps1/devlog/current-status-2026-03-21/</id>
    <published>2026-03-21T00:00:00-04:00</published>
    <updated>2026-03-21T00:00:00-04:00</updated>
    <author>
      <name>Hunter Davis</name>
      <uri>https://hunterdavis.com/</uri>
    </author>
    <summary>Restore-pilot-era status snapshot from 2026-03-21. Historical counts; not the current scene-playback bar.</summary>
    <content type="html"><![CDATA[<blockquote>
  <p><strong>⚠️ Historical snapshot — not current truth.</strong>
Dated 2026-03-21. Preserved as the restore-pilot-era status surface.
The “verified” counts below belong to the restore-rollout validation
model and are not comparable to the current scene-playback bar. For
current status see the <a href="../source/docs/ps1/scene-status/">scene-status shelf page</a>
and <a href="../source/docs/ps1/current-status/">current-status shelf page</a>.</p>
</blockquote>

<p>Date: 2026-03-21</p>

<ul>
  <li>verified scenes: <code class="language-plaintext highlighter-rouge">27 / 63</code></li>
  <li>live header scenes: <code class="language-plaintext highlighter-rouge">27 / 63</code></li>
  <li>live bring-up scenes not yet counted as verified: <code class="language-plaintext highlighter-rouge">0</code></li>
</ul>

<h2 id="verified">Verified</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">STAND.ADS</code> tags <code class="language-plaintext highlighter-rouge">[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16]</code> (14 scenes)</li>
  <li><code class="language-plaintext highlighter-rouge">JOHNNY.ADS</code> tags <code class="language-plaintext highlighter-rouge">[1, 2, 3, 4, 5, 6]</code> (6 scenes)</li>
  <li><code class="language-plaintext highlighter-rouge">WALKSTUF.ADS</code> tags <code class="language-plaintext highlighter-rouge">[1, 2, 3]</code> (3 scenes)</li>
  <li><code class="language-plaintext highlighter-rouge">MISCGAG.ADS</code> tags <code class="language-plaintext highlighter-rouge">[1, 2]</code> (2 scenes)</li>
  <li><code class="language-plaintext highlighter-rouge">MARY.ADS</code> tags <code class="language-plaintext highlighter-rouge">[3, 5]</code> (2 scenes)</li>
</ul>

<h2 id="bring-up">Bring-Up</h2>

<ul>
  <li>No scenes are currently in the live-header bring-up bucket. <code class="language-plaintext highlighter-rouge">ACTIVITY.ADS tag 4</code> remains a runtime issue, but it is not promoted in the current live header.</li>
</ul>

<h2 id="blocked--unreliable-entry-paths">Blocked / Unreliable Entry Paths</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BUILDING.ADS</code> still needs a trustworthy entry/validation path before promotion.</li>
  <li><code class="language-plaintext highlighter-rouge">FISHING.ADS</code> tag <code class="language-plaintext highlighter-rouge">1</code> still needs a trustworthy entry/validation path before promotion.</li>
</ul>

<h2 id="notes">Notes</h2>

<ul>
  <li>This status snapshot supersedes the archived 2026-03-19 rollout manifest for day-to-day planning.</li>
  <li>The generated header currently includes 27 promoted scene tags, all counted as verified.</li>
  <li><code class="language-plaintext highlighter-rouge">MARY.ADS</code> tags <code class="language-plaintext highlighter-rouge">3</code> and <code class="language-plaintext highlighter-rouge">5</code> are now verified live under the retained <code class="language-plaintext highlighter-rouge">MARY</code>-specific startup and restore-hook policy.</li>
  <li>The current pack-side artifact surface was refreshed on 2026-03-21:
<code class="language-plaintext highlighter-rouge">10</code> family packs, <code class="language-plaintext highlighter-rouge">560</code> compiled entries, <code class="language-plaintext highlighter-rouge">219</code> PSB entries, and
<code class="language-plaintext highlighter-rouge">22,743,040</code> bytes of compiled research payload under
<code class="language-plaintext highlighter-rouge">repo:/docs/ps1/research/generated/compiled_packs_2026-03-21</code>.</li>
</ul>
]]></content>
  </entry>
</feed>
