Devlog ·
PS1 Restore Rollout History
~12 min read · 3247 words
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.
Historical rollout chronology
The remainder of this document is preserved as a dated implementation history.
Use repo:/docs/ps1/research/CURRENT_STATUS_2026-03-21.md
for the active rollout snapshot; older dated notes below are useful for design
rationale, but not as the current source of truth.
2026-03-18 update
Phase 7 now has an offline extractor for dirty-region candidates:
repo:/scripts/extract-dirty-region-templates.pyrepo:/docs/ps1/research/DIRTY_REGION_TEMPLATE_SCHEMA.md
This produces per-pack template JSON from scene-pack manifests plus extracted
TTM 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.
An initial BUILDING.ADS runtime CLEAR_SCREEN 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.
The original scene-level pilot picked from that process is now archived in:
repo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_candidate_report_2026-03-18.jsonrepo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_pilot_spec_2026-03-18.json
At that stage, the active narrow targets were:
STAND.ADS tags 1-3, which has only one BMP, two TTM owners, and a352x140restore envelopeJOHNNY.ADS tag 1, which reuses the same generated-contract path through theMEANWHIL.TTM,SJMSSGE.TTM,SJWORK.TTM, andTHEEND.TTMclusterWALKSTUF.ADS tag 2, which exercisesMJJOG.TTM,MJRAFT.TTM, andWOULDBE.TTM, including a two-region clear/save contract inWOULDBE.TTM
Those pilots now have a generated C-side artifact:
repo:/ps1_restore_pilots.h
It is emitted from repo:/scripts/generate-restore-pilots-header.py
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.
The offline spec path is now batched too:
repo:/scripts/build-restore-pilot-spec.pycan emit one spec per recommended pilot intorepo:/docs/ps1/research/generated/restore_pilot_specs_2026-03-19repo:/scripts/generate-restore-pilots-header.pycan 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
That batching is now expanded to the full current ADS surface too:
repo:/docs/ps1/research/generated/restore_candidate_report_full_2026-03-19.jsonranks one recommended restore candidate per ADS familyrepo:/docs/ps1/research/generated/restore_pilot_specs_full_2026-03-19contains the first ten-family pre-calculated spec batch:STAND,JOHNNY,WALKSTUF,ACTIVITY,FISHING,BUILDING,VISITOR,MARY,MISCGAG, andSUZY- 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
That same pipeline now emits scene-scoped output too:
repo:/docs/ps1/research/generated/restore_scene_specs_full_2026-03-19contains one restore spec per ranked scene,63total, so offline conversion no longer waits on one-family-at-a-time promotionrepo:/docs/ps1/research/CURRENT_STATUS_2026-03-21.jsonis the active rollout snapshot for day-to-day workrepo:/docs/ps1/research/archive/2026-03-19-rollout-snapshot/restore_rollout_manifest_2026-03-19.jsonis the original grouped-rollout snapshot and is now archived for historical comparison onlyrepo:/docs/ps1/research/generated/restore_scene_clusters_2026-03-19.jsongroups those63scene specs into34shared restore contracts, which is the right unit for grouped runtime promotionrepo:/docs/ps1/research/generated/restore_cluster_specs_2026-03-19lifts those34contracts into reusable cluster specs, so the next runtime enablement step can promote a whole contract in one move
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 x=65534 /
width=65551 from the VISITOR family and regenerated the full scene-spec and
cluster-spec artifact set from corrected geometry.
With the full queue emitted, live promotion is now split between:
- live pilots that are actually reachable through the current harness path
- blocked pilots that remain offline-only until their story/bootstrap route is reproducible
The current verified rollout count is 25 / 63 scenes, with 26 scene tags
currently present in the live generated header. The active verified families
are STAND, JOHNNY, WALKSTUF, and MISCGAG; ACTIVITY.ADS tag 4 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
STAND cluster contract for tags 1-12, and bounded forced runs of
STAND.ADS 4 and STAND.ADS 12 both stayed visually good. This is the first
real contract-sized expansion beyond the original pilot tags.
The same grouped rollout now extends to JOHNNY too: the runtime header keeps
the original proven singleton for JOHNNY.ADS 1, adds the shared contract for
JOHNNY.ADS 2-5, and now also carries the validated JOHNNY.ADS 6 singleton
contract. Bounded forced runs of JOHNNY.ADS 2, JOHNNY.ADS 5, and
JOHNNY.ADS 6 all stayed visually good, so this grouped rollout now covers
the full JOHNNY.ADS 1-6 surface.
STAND has also moved beyond the first cluster. In addition to the shared
STAND.ADS 1-12 contract, the runtime header now carries the second shared
contract for STAND.ADS 15-16, and bounded forced runs of STAND.ADS 15 and
STAND.ADS 16 both stayed visually good. That makes STAND.ADS 1-12 plus
15-16 the current largest live grouped rollout.
BUILDING was tried as the next grouped target, but both island ads and
story ads 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.
WALKSTUF has now been widened to cover tags 1-3 in the live generated
header. Forced runs of WALKSTUF.ADS 1 and WALKSTUF.ADS 3 stayed visually
good, so the scene-scoped restore contracts are viable there too, but both
routes still report active-pack fallbacks. That makes WALKSTUF the next
cleanup target: the restore policy is holding, while the remaining debt is in
pack completeness/runtime reads rather than scene composition.
That next slice is now in place in narrowly-scoped form: ttm.c has a
scene-scoped CLEAR_SCREEN pilot hook for the STAND.ADS tags 1-3 pilot
cluster that only applies to MJAMBWLK.TTM and MJTELE.TTM using the
generated header rects, instead of another family-wide restore hook.
The pilot now also tracks TTM_UNKNOWN_1 region ids in thread state and uses
the same generated contract for SAVE_IMAGE1 on the narrow STAND.ADS pilot
cluster (tags 1-3).
The validation harness has been tightened around real PS1 boot timing too:
forced STAND 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 STAND path comes up reliably and the decoder reports
pilot_pack ... fallbacks=0.
With that harness in place, the next Phase 7 cut is also live: on the same
STAND.ADS tags 1-3 route, ads.c no longer uses replay merge, actor
recovery, or handoff carry/injection as a correctness mechanism. A fresh forced
STAND.ADS 1 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.
That same replay-policy cut now also goes through the JOHNNY.ADS tag 1
pilot via the shared generated pilot table. A fresh forced
JOHNNY.ADS 1 run still held with pilot_pack ... fallbacks=0.
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
repo:/ads.c: adsPlayWalk() now resets
ttmSlots[0] after adsStopScene(0), so stale JOHNWALK 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.
One important validation detail from that same run: story phase=4 persisting
for several captures is expected while the handoff is still inside
adsPlayWalk(). storyPlay() only advances to phase 5 after the walk path
returns, and the walk code intentionally holds an arrival pose before it exits.
So the remaining long phase=4 window is not by itself evidence of another
stuck story-state bug.
One useful validation note from that route: the black-backed clock in the
MEANWHIL sequence is not a new restore regression. The original
MEANWHIL.TTM script explicitly issues DRAW_RECT 0 0 640 350 after
SET_COLORS 5 5, then repeatedly draws the clock backing sprite. So that card
is authored scene behavior, not a pack-path failure.
The next generated pilot now exists too:
repo:/docs/ps1/research/archive/2026-03-18-pilot-artifacts/restore_pilot_spec_walkstuf_2026-03-18.json
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.
The pilot specs now carry explicit scene resource lists too, and the PS1
runtime primes those scene-scoped resources before play through
repo:/ps1_restore_pilots.h
and repo:/ads.c. 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.
One current validation note: the pack fallback telemetry is now wired to real
extracted-file reads instead of stale counters. STAND.ADS still validates with
pilot_pack ... fallbacks=0, and the WALKSTUF.ADS 2 first-frame actor gap is
now fixed by re-enabling only one-frame missing-actor recovery on that pilot
route. The opening frame at
ps1-test-20260319-083913.png now shows Johnny holding the board where the
earlier ps1-test-20260319-083316.png run showed only the board. WALKSTUF
still reports one real active-pack miss (WOULDBE.BMP signature 17) during
the bounded harness route, but that remaining miss is now a secondary cleanup,
not the visible scene-entry regression.
On this page
- Historical rollout chronology
- Current facts from the repo
- 1. The PC and PS1 rendering models do not match
- 2. The project already uses ISO space to trade for RAM, but inconsistently
- 3. Static analysis is already strong enough to drive compilation decisions
- 4. Analyzer v2 output is now machine-readable
- 5. Pack planner consumer exists
- 6. Scene pack compiler and generic loader exist
- 7. Transition / prefetch post-processing
- 8. Dirty-region and restore runtime
- 9. SDL-Compat Lite contract
- Recommended architecture
- What should move offline
- What should stay runtime
- Research conclusions
- External references
- Related files in this research package
Current facts from the repo
1. The PC and PS1 rendering models do not match
PC path:
- Composes
background + savedZones + thread layers + holiday layer. - Uses persistent per-thread
ttmLayersurfaces. - Relies on direct sprite blits plus save/restore zone behavior.
Key files:
repo:/graphics.crepo:/graphics.h
PS1 path:
- Restores clean background tiles, composites indexed sprites into RAM tiles, then uploads those tiles every frame.
- Keeps replay records in thread state and rebuilds visual continuity from those records.
- Has partial or empty implementations for several SDL-like primitives and save/restore calls.
Key files:
repo:/graphics_ps1.crepo:/graphics_ps1.hrepo:/ads.c
2. The project already uses ISO space to trade for RAM, but inconsistently
The PS1 port already bypasses resource-file decompression for hot assets and loads pre-extracted files from disc:
BMP/SCR/TTM/ADS/
Key file:
repo:/cdrom_ps1.c
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.”
3. Static analysis is already strong enough to drive compilation decisions
The scene analyzer already computes:
- per-scene BMP/SCR/TTM/ADS usage
- estimated peak memory
- sprite frame counts
- concurrent thread counts
- global heavy-scene rankings
- machine-readable JSON for build-time tooling
- derived heuristics for scene clustering, shared resources, transition churn, and ADS-family prefetch candidates
Key files:
repo:/scene_analyzer.crepo:/scripts/analyze-scenes.shrepo:/Makefile.analyzer
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.”
4. Analyzer v2 output is now machine-readable
scene_analyzer now supports --json in addition to the original text report.
Command:
./scripts/analyze-scenes.sh --json > docs/ps1/research/generated/scene_analysis_output_2026-03-17.json
Current schema highlights:
summary- global heaviest-scene and concurrency maxima
derived.candidate_scene_clusters- current heuristic groups scenes by ADS file as a pack-compilation starting point
derived.shared_resources- BMP/TTM inventories shared across multiple scenes
derived.heaviest_transition_deltas- highest-churn sequential scene-to-scene deltas for pack-boundary review
derived.likely_prefetch_sets- ADS-family union heuristic for first-pass prefetch planning
scenes[*]- story metadata, resource bindings, thread launches, and explicit memory components
Important caveat:
- transition and prefetch outputs are currently heuristic rather than proven runtime transition graphs
- pointer-table accounting is now explicitly PS1-sized at
4bytes per pointer, rather than using hostsizeof(void *)
5. Pack planner consumer exists
The first build-facing consumer of the analyzer JSON is now checked in:
./scripts/plan-scene-packs.py \
--output docs/ps1/research/generated/scene_pack_plan_2026-03-17.json \
--manifest-dir docs/ps1/research/generated/scene_pack_manifests_2026-03-17
What it emits:
- one aggregate plan file for the pack compiler
- one manifest per ADS-family pack
- per-pack resource aggregates with global and pack-local reference counts
- transition-driven prefetch candidates, with ADS-family fallback still available
Important caveat:
- this is a planning consumer, not a runtime loader
- the pack IDs and prefetch links are heuristic, derived from the analyzer JSON
- the manifests are intentionally shaped to make the later compiler a mechanical step rather than a discovery step
6. Scene pack compiler and generic loader exist
The compiler now consumes either one manifest or the full manifest directory and emits concrete compiled packs for every current ADS family:
./scripts/compile-scene-pack.py --all
Current outputs:
repo:/scripts/compile-scene-pack.pyrepo:/docs/ps1/research/PACK_PAYLOAD_LAYOUT.mdrepo:/docs/ps1/research/generated/compiled_packs_2026-03-17
What it emits:
pack_payload.bin- deterministic raw resource blob
pack_index.json- sector-aligned offsets, sizes, checksums, and runtime envelope metadata
jc_resources/packs/*.PAK- staged CD-visible payloads for all current ADS families, each with a compact binary TOC at the front of the file
Current constraints:
- resource order is fixed as
ADS -> SCR -> TTM -> BMP - each resource is aligned to
2048bytes - 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
Runtime hook:
- ADS loads now activate a pack-first lookup path in
repo:/cdrom_ps1.c - all current
ACTIVITY/BUILDING/FISHING/JOHNNY/MARY/MISCGAG/STAND/SUZY/VISITOR/WALKSTUFfamilies now try their staged pack payload first - once an ADS-family pack is active,
ADS/SCR/TTM/BMPpayloads are all pack-authoritative and no longer fall back to the extracted-file path - bounded DuckStation validation now decodes
pilot_pack active_pack_id=7 hits=7 fallbacks=0on the working scene path, so the pack path is active without observed extracted-asset fallback in that traversal - the generated lookup table is now shared rather than BUILDING-specific
7. Transition / prefetch post-processing
The analyzer JSON is now fed through a small post-processor that turns the raw scene sequence into more actionable planning output:
repo:/scripts/scene-transition-prefetch-report.pyrepo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.jsonrepo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.mdrepo:/docs/ps1/research/TRANSITION_PREFETCH_SCHEMA.md
This post-processor adds:
- pack candidates with unioned resource bytes
- adjacent transition edges with added/shared/removed byte counts
- ranked prefetch edges based on added working set
8. Dirty-region and restore runtime
The PS1 runtime now has its first explicit region-restore implementation instead of relying entirely on whole-frame background restore plus replay continuity:
repo:/graphics_ps1.cnow implementsgrSaveZone()/grRestoreZone()against the clean background tile copies- the implementation tracks one active saved zone, matching the existing PC-side assumption for Johnny’s TTMs
RESTORE_ZONEcan now restore a bounded rectangle from the pristine background tiles during TTM playback rather than acting as a no-op on PS1- bounded DuckStation validation still boots the working scene and decodes
pilot_pack ... fallbacks=0after this change
9. SDL-Compat Lite contract
The first written contract for the narrow runtime boundary now lives in:
repo:/docs/ps1/research/SDL_COMPAT_LITE_SPEC.md
It captures:
- the minimum gameplay-facing graphics surface
- the current PC/PS1 gap matrix
- the places where PS1 still leaks replay-era implementation details into gameplay-visible correctness
- ranked pack-boundary candidates for disc-layout review
The caveat remains the same: these are story-order planning heuristics, not a validated runtime transition graph.
Recommended architecture
Recommendation A: Compiled scene packs
This is the highest-leverage option.
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.
Each pack should contain:
- resolved TTM set
- scene-local sprite banks
- pretranscoded sprite frame data
- precomputed metadata for thread maxima
- precomputed memory envelope for enter / steady / exit phases
- optional prefetch links to likely next packs
Runtime effect:
- less generic resource management
- fewer live asset formats
- deterministic memory behavior
- less reason for replay heuristics
Tradeoff:
- more ISO use
- more offline tooling
- need to choose pack granularity carefully
Recommendation B: Offline sprite transcoding to PS1-native blit format
Convert every sprite frame offline into a format optimized for the actual PS1 draw path, not for the original file format.
Strong candidates:
- opaque span tables per scanline
- simple RLE by span
- pre-flipped spans for horizontal mirroring
- nibble order fixed ahead of time
- tile split decided offline rather than at load time
Why this matters:
- runtime sprite draw becomes a deterministic span blitter
- fewer per-pixel branches
- fewer chances to disagree with PC semantics
- removes load-time format quirks from gameplay execution
This is a better use of ISO space than repeatedly paying for runtime interpretation.
Recommendation C: SDL-Compat Lite runtime contract
Define a narrow runtime API that matches what the engine actually needs.
Minimum contract:
- acquire/release logical layer
- begin frame / present frame
- draw sprite / draw flipped sprite
- draw rect / line / pixel
- set clip zone
- copy/save/restore zone
Behavioral guarantees:
- per-thread layer persistence
- transparent blit semantics
- deterministic present order
- working save/restore for at least the current script patterns
The main design rule is:
Do not let ADS/TTM gameplay logic know about replay records, actor recovery, or background-tile juggling.
Recommendation D: Scene-local frame cache with static prefetch
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.
That means:
- keep only scene-local active frames in RAM
- prefetch likely next frames or next scene pack
- evict by deterministic schedule, not general LRU guesswork
This is weaker than compiled packs, but still much better than “runtime discovers everything dynamically.”
What should move offline
These are the best candidates for static or build-time computation:
- Sprite frame transcoding
- Convert BMP sprite sheets into PS1-native draw records.
- Flip variants
- Precompute flipped versions when that reduces runtime branches and edge cases.
- Tile splitting
- Decide how frames are split across 64px texture constraints offline.
- Dirty-region templates
- Precompute common dirty rectangles for stable sequences like walking, fire, fish, coconut, and note scenes.
- Scene memory envelopes
- Emit per-scene “must reside” and “can stream” sets.
- Transition handoff data
- Emit exact persistence requirements at walk-to-scene and scene-to-scene boundaries.
- CD layout hints
- Group pack files physically to reduce seek penalties.
What should stay runtime
Runtime should be limited to:
- running ADS and TTM logic
- opening the correct compiled pack
- drawing from a deterministic surface API
- small bounded frame caching if needed
- presenting layers in a fixed order
Runtime should not keep growing new correctness heuristics for:
- actor continuity
- replay recovery
- draw-record resurrection
- dynamic sprite identity matching
Those are symptoms of the wrong boundary.
Research conclusions
Best target state
The strongest long-term target is:
Compiled scene packs + offline-transcoded sprites + SDL-Compat Lite runtime
That combination gives the biggest simplification win and aligns with the fact that Johnny Castaway content is fixed, finite, and highly analyzable.
Best incremental path
If the team wants lower risk, use this order:
- Extend analyzer to emit machine-readable scene data.
- Build an offline sprite transcoder for one problematic content family.
- Implement SDL-Compat Lite boundary.
- Route one scene family through compiled packs.
- Expand scene-pack coverage once correctness is proven.
What not to do
Avoid investing heavily in deeper versions of the current replay-based model.
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.
External references
These are useful for the build/runtime tradeoff discussion:
- PSX-SPX CD-ROM drive notes: https://psx-spx.consoledev.net/cdromdrive/
- PSX-SPX MDEC reference: https://psx-spx.consoledev.net/macroblockdecodermdec/
- PSn00bSDK texture / CLUT tutorial mirror: https://www.breck-mckye.com/psnoobsdk-docs/chapter1/3-textures.html
Why they matter:
- CD reads are fast enough to support deliberate streaming, but need disciplined buffering and sector handling.
- 4-bit indexed textures and CLUT constraints are native strengths of the PS1.
- MDEC exists, but it is a specialized JPEG-style path and is not automatically the right fit for general sprite animation assets.
Related files in this research package
repo:/docs/ps1/research/BACKLOG.mdrepo:/docs/ps1/research/IMPLEMENTATION_PLAN.mdrepo:/docs/ps1/research/PACK_MANIFEST_SCHEMA.mdrepo:/docs/ps1/research/PACK_PAYLOAD_LAYOUT.mdrepo:/docs/ps1/research/generated/scene_analysis_output_2026-03-17.txtrepo:/docs/ps1/research/generated/scene_analysis_output_2026-03-17.jsonrepo:/docs/ps1/research/generated/scene_pack_plan_2026-03-17.jsonrepo:/docs/ps1/research/generated/scene_pack_manifests_2026-03-17repo:/docs/ps1/research/generated/compiled_packs_2026-03-17repo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.jsonrepo:/docs/ps1/research/generated/scene_transition_prefetch_report_2026-03-17.mdrepo:/docs/ps1/research/TRANSITION_PREFETCH_SCHEMA.md