# PSX Art-Binding Recovery ## Scope - Active target: retail PlayStation `SLUS_002.68` feeding the renderer-local cache pipeline in `Crusader-Map-Viewer/map_renderer`. - Goal of this pass: stop treating the current unreadable PSX output as a renderer-only problem and measure whether placeholders are mainly caused by extraction-time art binding failures. - This note records the first pragmatic recovery pass that replaces large placeholder bands with real bundle-backed art while keeping the result auditable in exported scene metadata. ## Root Cause Summary - The current unreadable output was primarily an extraction-side problem, not a front-end draw bug. - Before this pass, the built `.cache/scene-cache/psx-remorse` set carried `58,262` fallback placeholders versus only `1,714` bundle-mapped items. - The fallback concentration was overwhelmingly in `section0_constructor_placements` (`94.5%` of fallback items), not in the smaller `section0_dispatch_roots` family. - The hottest unresolved types were `0x0042`, `0x0041`, `0x0047`, `0x0045`, `0x003f`, `0x004b`, `0x0046`, `0x0044`, `0x0048`, and `0x004a`. - Those types already appeared in the exported `DAT_800758d8` art-template layer in many maps, but almost all of their rows were `blockSize = 0` and therefore had no direct payload dwords to match against bundle offsets. - The neighboring state banks were still populated: `DAT_800758cc` carried valid script tables for the same types, while `DAT_800758d0` stayed empty for this family and `DAT_800758d4` remained on the runtime-bounds side. - That combination strongly suggests inherited or aliased presentation for these constructor-placement families rather than a literal absence of art. ## Implemented Recovery Rule The cache builder now resolves PSX art in three stages instead of dropping directly from "no direct `DAT_800758d8` payload" to a placeholder: 1. Use the direct non-zero `DAT_800758d8` row when a type has a real local template payload and bundle match. 2. If the type lives in the unresolved zero-block constructor-placement family, look for a same-map donor type whose `DAT_800758cc` script blob is identical and whose `DAT_800758d8` row resolves to a real bundle. 3. If no exact script-signature donor exists, fall back to the nearest resolved same-map donor inside the current constructor-placement family band (`0x003e..0x0064`). The exporter keeps this explicit instead of pretending the rule is fully solved: - `mappingSource` now records whether the art came from a direct template, a `cc-signature-donor:xxxx`, or a `generic-family-donor:xxxx` path. - `templateTypeId` and `donorTypeId` are preserved in exported `mapSource` rows. - The unresolved type still uses its own `DAT_800758cc` selector-to-frame map; only the bundle source is borrowed. ## Measured Effect Two measured rebuilds mattered in this pass. ### First donor pass - Added donor reuse for the original generic family band. - Totals moved from `58,262` fallback / `1,714` bundle-mapped to `37,054` fallback / `22,922` bundle-mapped. Representative maps: - `map 0`: `1078 / 111` -> `340 / 849` - `map 9`: `664 / 111` -> `197 / 578` - `map 43`: `707 / 97` -> `196 / 608` - `map 64`: `1194 / 161` -> `736 / 619` - `map 104`: `909 / 93` -> `894 / 108` ### Extended donor band - Extended the same heuristic through the next zero-block constructor-placement band up to `0x0064`. - Final measured totals after rebuild: `25,038` fallback / `34,938` bundle-mapped. Representative maps after the extended pass: - `map 0`: `66` fallback / `1123` bundle-mapped - `map 9`: `42` fallback / `733` bundle-mapped - `map 43`: `9` fallback / `795` bundle-mapped - `map 64`: `160` fallback / `1195` bundle-mapped - `map 104`: `866` fallback / `136` bundle-mapped So the first practical conclusion is straightforward: the exporter was the main blocker for most maps, and a constrained donor heuristic can replace a large fraction of placeholders with real graphics without touching the viewer runtime. ## No-Placeholder Cohort Pass - A second follow-up pass replaced synthetic fallback atlas generation with cohort-aware donor resolution keyed by authored family plus raw `u5` lane before the older coarse type default is consulted. - The exporter still prefers direct local `DAT_800758d8` payload matches first, but when a type bucket is mixed-role or otherwise unresolved it now resolves each cohort separately instead of collapsing the whole `map:type` bucket into one donor or one placeholder. - Focused validation on `map 104` now rebuilds as `1002` real-art items with `0` fallback items, `1` atlas, and `136` shape definitions. Scene `sourceCounts` are now `52` `section0_dispatch_roots-art` plus `950` `section0_constructor_placements-art`. - The scene JSON also now preserves `mappingSource` and optional `artCohort` on each `mapSource` row, so provisional cohort and emergency donor matches stay auditable after placeholders are removed. - This is still not the final executable-proof binding rule. The focused cache is now placeholder-free, but many rows still resolve through `cohort-*` or `emergency-global-donor:*` provenance and should be treated as provisional PSX art recovery rather than as final family/resource closure. ## Remaining Unresolved Mass - The cache is still not fully closed. `25,038` fallback items remain across `62` maps. - The remaining fallback distribution is still concentrated in `section0_constructor_placements`. - Current top fallback-heavy types after the donor pass are `0x0042`, `0x005a`, `0x005b`, `0x005c`, `0x0047`, `0x0059`, `0x005d`, `0x005e`, `0x0062`, and `0x0063`. - `0x0042` remains the single largest unresolved type even after the heuristic pass. - `map 104` is the most obvious current outlier: it still sits at `866` fallback items versus only `136` bundle-mapped items, so the next recovery pass should treat it as the best stress case rather than relying only on the now-mostly-readable early maps. Those counts are now historical rather than current for the focused `map 104` export. They remain useful as the baseline that motivated the cohort pass, but the immediate blocker has shifted from “remove placeholders” to “replace provisional donor provenance with tighter executable-backed cohort/resource rules.” ## Practical Interpretation - The donor heuristic is good enough to prove that many placeholders were caused by missing extraction-time inheritance logic. - It is not strong enough to count as the final executable-backed rule for the unresolved families. - The remaining gap still sits where the earlier Ghidra work already pointed: somewhere between the constructor-side art/resource creation lane and the live post-spawn state/resource/frame reselection path. ## Palette Lock-In Rule - The PSX exporter must not treat `mode 1` bundle header palette index `+0x14` as the rendered 256-color selector. - The current dump-grounded rule is narrower and stronger: the known-good visible family decodes against one contiguous 256-entry lookup table equivalent to live VRAM row `0xF0`, `x=0`. - In extractor terms, that behaves like the first `16` adjacent `16-color` CLUTs flattened into one 256-entry palette. That is the rule now encoded in `map_renderer/src/lib/psx-cache.js` for `mode 1` bundles. - The old bundle-header palette index is still preserved in exported scene metadata as `defaultPaletteIndex`, but it is diagnostic provenance, not the primary rendered selector. - `mode 2` stays on the earlier per-bundle/per-usage palette-index path. This lock-in applies specifically to the current `mode 1` family proven by the VRAM dump. - If a future pass proves a wider runtime CLUT-row formula, update this note and the exporter together in one change. Do not silently reintroduce `mode 1 -> bundle header palette index` as a fallback rule. ## Static Export Follow-Up - The processed PSX catalog already carried `62` maps during this pass, so the "single map" symptom was not a cache-build enumeration failure. - The immediate export-side blocker was config: `psx-remorse` was excluded from static export even though the prebuilt catalog/build-manager path already supports multi-map PSX scenes. - The renderer config now includes `psx-remorse` in static export so full PSX exports can surface the full processed map set instead of dropping the version entirely. ## Live MCP Follow-Up (2026-04-13): Main-Visible Palette-Token Submit Normalization - Scope for this focused pass: exact submit-flag packing and pre-submit normalization centered on `psx_draw_main_visible_object` (`0x80041458`), wrapper context in `psx_lset_world_frame_wrapper` (`0x80031f0c`) and `psx_draw_world_visible_passes` (`0x80041378`), with submitter override gates at `0x80044e10` and `0x80044eb8`. - Main-visible now has instruction-level confirmation for flag packing at the call sites (`0x800415c0` / `0x800415e0`): - `a3 = (obj_flags & 0x0002) | token_hi` - `token_hi` is normalized earlier as `source_palette_word & 0xFF00` at `0x80041590`. - Exact palette-token carriage for this lane is therefore bits `15:8` of submit flags (`token = flags >> 8`). Low nibble bits remain non-palette control bits. - Submitter override gate behavior is now explicitly aligned across image-table and sprite submitters: - gate expression is `(flags & ~0xF) != 0` (equivalent to `(flags & 0xfffffff0) != 0`) - because world callers only contribute `obj_flags&0x0002` plus token high byte, bit `0x0002` alone does not trigger override; nonzero token high byte is the effective palette-override activator. - Non-obvious normalization outcome for exporter logic: no additional wrapper-stage or no-op-hook mutation exists before `0x80041458` submits; palette-relevant normalization is local to main-visible draw and consists of high-byte extraction only. Conservative live-artifact updates applied in Ghidra for this pass: - nearby helper renames: - `0x8003a3b0 -> psx_world_draw_tint_fade_step` - `0x80038f10 -> psx_noop_frame_hook_38f10` - `0x80044018 -> psx_noop_frame_hook_44018` - targeted decompiler comments: - `0x80041590` (token high-byte normalization) - `0x800415c0` (final submit-flag packing) - `0x80044e10` / `0x80044eb8` (shared override gate and token indexing) - `0x80031f34` / `0x80031f3c` (wrapper no-op hooks do not mutate submit flags) ## Live MCP Follow-Up (2026-04-13): World Draw Pass + CLUT Routing Refresh - Scope for this focused pass: live `SLUS_002.68` world draw and submitter routing at `0x80041378`, `0x80041458`, `0x80041144`, `0x80044bdc`, and `0x80044e9c`, plus CLUT table lanes `0x800a9f48` and `0x800a9f66`. - World draw pass ordering remains fixed and explicit: stage-1 main-visible sorted slice first, then stage-2 special-visible queue, then HUD/overlay. - Stage-1 and stage-2 world lanes still share submitter dispatch by bound resource kind (`kind==5` image-table submitter, otherwise sprite submitter). - Palette-token handling remains lane-split and exporter-critical: main-visible injects authored high-byte palette token into submit flags; special-visible does not. - CLUT override gate remains shared (`submit_flags & 0xfffffff0`), but CLUT table resolution still branches by submitter/resource-format lane: - `psx_image_table_submit_frame`: high-byte token selects `psx_clut_override_table_by_palette_token[token]`, otherwise default bank CLUT. - `psx_sprite_resource_submit_frame`: format-2 lane follows override table path; non-format-2 lane remaps token through bank CLUT indexing. - Nearby anonymous helper cleanup in the same draw wrapper lane: - `0x8002e534 -> psx_marker_channel_runtime_get_u16_86` - `0x8002eee8 -> psx_marker_channel_runtime_get_u16_84` Conservative live-artifact updates applied in Ghidra for this pass: - `0x80041378`: decompiler comment clarifying stage-1 -> stage-2 -> HUD order. - `0x800415c0`: decompiler comment clarifying main-visible authored token injection before image-table submit. - `0x800412dc`: decompiler comment clarifying special-visible omits authored high-byte token injection. - `0x80044ed0`: decompiler comment clarifying image-table CLUT override-table path. - `0x80044e5c`: decompiler comment clarifying sprite default bank-CLUT path when override gate is inactive. ## Live MCP Follow-Up (2026-04-12): Wall-Family Split Failure Mode - Scope for this focused pass: live `SLUS_002.68` around the wall-heavy generic family band (`0x003e..0x004f`) and exporter donor-heavy bundles (`0x0008b48c`, `0x00085c40`). - Constructor-side bind is now explicit and consistent across this family: both `psx_object_create_simple_record` (`0x800249f4`) and `psx_object_create_compound_record` (`0x80024eec`) read the per-type active art header from `DAT_800758d8[type]` before any lane-orientation updates. - That means cross-family visual collapse is unlikely to be caused by a missing pre-constructor resource-bank split in this band; divergence happens after bind. - Post-bind state/frame path is also explicit: `psx_object_select_state_script` (`0x800260e8`) installs selector state (`obj+0x9e` and script cursor `obj+0x90`), then `psx_object_advance_state_script` (`0x80025d68`) latches live frame token `obj+0x94` from the script stream. - Draw submission consumes that latched token, not raw authored selector: stage-1 geometry (`0x80040d44`) and stage-2 geometry (`0x80040f78`) both query frame geometry from `obj+0x94`, and submitters read the same token in `psx_draw_main_visible_object` (`0x80041458`) / `psx_draw_special_visible_queue` (`0x80041144`). - The stage-1/stage-2 route split matters materially: main-visible injects authored palette token only for type bands `>=0x003e`, while special-visible does not. If exporter cohorts collapse route outcome, families can appear as repeated art with wrong face/palette behavior. - Practical exporter implication: the strongest missing discriminator is runtime route+latch state (effective selector/latch outcome), not a new type-level resource bank key. Conservative live-artifact updates applied in Ghidra for this pass: - `0x80046038`: comment clarifying `DAT_800758d8[type]` install timing and post-bind divergence expectation. - `0x80026100`: comment clarifying pre-latch selector install and risk of authored-`u4`-only grouping. - `0x80041554`: comment clarifying main-visible-only authored palette-token lane for `>=0x003e`. - `0x80040f88`: comment clarifying stage-2 queue still consumes `obj+0x94` but has different route semantics. ## Live MCP Follow-Up (2026-04-12): CLUT Override Routing Closure - Scope for this focused pass: world-object draw path and submitters at `0x80041458`, `0x80041144`, `0x80044bdc`, and `0x80044e9c`, with CLUT table symbols at `0x800a9f48` and `0x800a9f66`. - `0x800a9f48` remains `psx_clut_table_by_resource_bank` and is both load-populated (`level_palette_upload_cluts`) and draw-consumed. - `0x800a9f66` remains `psx_clut_override_table_by_palette_token` and is draw-consumed by both submitters. - Main-visible (`0x80041458`) injects authored high-byte palette token into submit flags for type bands `>=0x003e`; special-visible (`0x80041144`) does not inject authored token and only forwards `obj_flags & 0x0002`. - Submitter override gate is explicit in both submitters: override is used only when `(submit_flags & 0xfffffff0) != 0`. - For this world-object path, token `0` behaves as "no override": with only low flag bits present, submitters fall through to default bank CLUT lookup. - `psx_image_table_submit_frame` uses token as a key into `psx_clut_override_table_by_palette_token[token]` when override is active. - `psx_sprite_resource_submit_frame` has two override lanes based on resource format field (`resource+4`): - format `== 2`: token keys `psx_clut_override_table_by_palette_token[token]`. - format `!= 2`: token is used as a row key into `psx_clut_table_by_resource_bank` (`token << 4` halfword index). Exporter-facing implication from executable evidence: - The current flattening bug matches a real semantic split in executable code: authored token injection is lane-dependent (main-visible only) and CLUT resolution is submitter/resource-format dependent. - A single flattened token->CLUT mapping in exporter code will miscolor mixed cohorts where route lane or submitter/resource format differs. Conservative live-artifact updates applied in Ghidra for this pass: - `0x800415b0`: comment clarifying main-visible token injection source and band split. - `0x800412d0`: comment clarifying special-visible non-injection behavior. - `0x80044e10`: comment clarifying sprite submit override gate and token-0 fallthrough. - `0x80044eb8`: comment clarifying image-table override keying behavior. ## Live MCP Follow-Up (2026-04-13): Selector Install, Transition Selection, and Final Latch Closure - Scope for this focused pass: selector-install and post-construction reselection chain centered on `psx_object_select_state_script` (`0x800260e8`), `psx_object_advance_state_script` (`0x80025d68`), `psx_object_select_state_from_transition_table` (`0x8001bca0`), `psx_type42_transition_selector_tick` (`0x80018578`), and delayed type-4 reselection around `0x8002906c`. - `psx_object_select_state_script` is confirmed as install-only in exporter terms: it writes selector `obj+0x9e`, seeds state-script cursor (`obj+0x8c/0x90`), and does not write final visible frame token. - Final visible frame/state token is latched in `psx_object_advance_state_script` where current script word is copied to `obj+0x94`; projection/draw lanes consume this live token. - Transition-table selection remains two-stage and row-driven: transition code from `psx_type_transition_mode_policy_rows` (`0x80063a00`) selects selector base from `psx_type_transition_selector_rows` (`0x80063b4c`), then `psx_object_select_state_script` installs selector. - In that transition path, runtime flag mutation is narrow and explicit: selector logic toggles `obj+0x1c` bit `0x0002` only; broad authored lane bits such as `0x0020` are not synthesized by this function. - `psx_type42_transition_selector_tick` adds an early pre-latch gate before reseat/turn-driven selector dispatch: object must be within view margin and pass the object-lane `obj+0x1c & 0x0020` condition. Selector updates there still occur before the later `obj+0x94` latch point. - The unresolved `FUN_8002906c` path is now closed by symbol state in live Ghidra: `0x8002906c` is `psx_type4_reselect_motion_state`, reached from `psx_type4_update_delayed_interaction` (`0x80029dac`) when delayed countdown reaches trigger. This is a post-construction reselection lane, not constructor-side initial bind. Exporter-facing implication from executable evidence: - A standalone JS exporter should treat selector install (`obj+0x9e`) and final latch (`obj+0x94`) as distinct channels. - Transition-table and type-`0x0042` reselection may alter pre-latch selector/runtime flag state without directly proving final frame token at draw time. - Cohort split logic should therefore prioritize latched `obj+0x94` capture (or strong proxy) over authored selector or transition slot alone. Conservative live-artifact updates applied in Ghidra for this pass: - `0x800260e8`: comment clarifying install-only selector semantics and no direct `obj+0x94` write. - `0x80025d68`: comment clarifying final frame/state latch into `obj+0x94`. - `0x8001bca0`: comment clarifying two-stage transition-row lookup and `0x0002`-only bit toggle scope. - `0x80018578`: comment clarifying type-`0x0042` pre-latch gate and reseat ordering. - `0x8002906c`: comment clarifying delayed post-construction reselection role. - `0x80029dac`: comment clarifying delayed countdown trigger into post-construction reselection. ## Live MCP Follow-Up (2026-04-13): Authored Family Descriptor Convergence and Constructor Bind Closure - Scope for this focused pass: authored section-0 family dispatch and constructor bind semantics on active `SLUS_002.68` at `psx_dispatch_section0_dispatch_roots` (`0x800256b0`), `psx_dispatch_section0_constructor_placements` (`0x800258cc`), `psx_object_create_simple_record` (`0x800249f4`), `psx_object_create_compound_record` (`0x80024eec`), and descriptor row `0x800626f8`. - Section-0 root and constructor-placement records are now reconfirmed as one convergence lane for unresolved families (`0x0042`, `0x0049`, `0x0055..0x0063`): both dispatch through descriptor slot0 and converge on row `0x800626f8` callback `0x80013618`. - Descriptor row role at `0x800626f8` is now explicitly preserved in disassembly comments as: - slot0 `0x80013618` (`psx_spawn_compound_record_advance_state_once`) - slot1 `0x80013688` (`psx_object_refresh_main_visible_and_cleanup`) - slot2 `0x800254c8` (`psx_object_release_to_free_list`) - Constructor bind semantics are now restated at function entry for exporter extraction: - simple path copies authored route word `record+0x10 -> obj+0x1c` - compound path copies authored route word `record+0x0A -> obj+0x1c` - both read active art header from `DAT_800758d8[type]` - both reuse `DAT_800758c8[type]` for kind-5 resources and otherwise build per-instance resource. - Practical exporter consequence: authored-family divergence should continue to be modeled post-constructor (state/policy/route/latch channels), not as a section-0 descriptor callback split. Conservative live-artifact updates applied in Ghidra for this pass: - `0x80031c34`: `FUN_80031c34` renamed to `psx_spawn_type0b_compound_burst_for_active_object_sweep`. - `0x800256b0`: decompiler comment clarifying root-dispatch descriptor convergence. - `0x800258cc`: decompiler comment clarifying constructor-placement descriptor convergence. - `0x800249f4`: decompiler comment clarifying simple constructor route-word copy and art-bind/cache split. - `0x80024eec`: decompiler comment clarifying compound constructor route-word copy and art-bind/cache split. - `0x800626f8`: disassembly comment clarifying shared descriptor-row slot mapping. ## Live MCP Follow-Up (2026-04-13): Source-Record Palette Token Provenance (`obj+0xa0`) and Region00 12-byte Fit - Scope for this focused pass: live `SLUS_002.68` constructor/source-pointer storage and world main-visible token read at `0x80024b50`, `0x80025048`, `0x800258cc`, and `0x80041458`. - Main-visible draw token read is now source-exact and band-split: - for `0x003e..0x00ab`: token high byte from `(*(obj+0xa0)+0x06) & 0xff00` - for `>=0x00ac`: token high byte from `(*(obj+0xa0)+0x0c) & 0xff00` - Constructor storage of `obj+0xa0` is now instruction-explicit in both create paths: - simple record path writes source pointer at `0x80024b50` - compound/region00 path writes source pointer at `0x80025048` - Section0 constructor-placement dispatch (`0x800258cc`) steps records by `+0x0c`, confirming region00-style authored records are 12 bytes in this lane. - For currently visible unresolved families in this workflow (`0x0042`, `0x0049`, `0x0055..0x0063`), type band is `<0x00ac`, so main-visible token read uses source offset `+0x06` only; this offset is inside 12-byte records and is therefore a usable authored palette-token carrier. - Consequence for exporter assumptions: treating region00 12-byte records as inherently unable to carry palette override bits is incorrect for these current families; only the `>=0x00ac` `+0x0c` read requires a longer source layout. Conservative live-artifact updates applied in Ghidra for this pass: - `0x80027f38`: `FUN_80027f38` renamed to `psx_alloc_runtime_snapshot_record_payload`. - `0x80024b50`: decompiler comment clarifying simple-path `obj+0xa0` source-record pointer storage and later main-visible `+0x06/+0x0c` token reads. - `0x80025048`: decompiler comment clarifying compound-path `obj+0xa0` storage and 12-byte-record compatibility with `<0x00ac` `+0x06` token read. - `0x8004156c`: decompiler comment clarifying the exact type-band split and source offsets used for main-visible palette-token injection. ## Next Steps 1. Trace the remaining high-volume band `0x0055..0x0063` in Ghidra with the same question used for `0x0042`: why does `DAT_800758d8` stay zero-sized while visible art still exists at runtime? 2. Use `map 104` as the primary regression target and dump its remaining fallback type/state/lane distribution before doing any broader heuristic expansion. 3. Compare unresolved zero-block types against nearby resolved donor types at the constructor/resource level, not only at the script-signature level, so borrowed bundles can be replaced with an executable-backed alias rule. 4. Keep the `DAT_800758d4` work on the bounds side unless a family-specific caller proves otherwise; this pass did not reopen that conclusion. ## Live MCP Follow-Up (2026-04-13): Art Payload Decode Semantics For Standalone Exporters - Scope for this focused pass: active PSX `SLUS_002.68` type-4/type-5 install and submit corridor centered on `psx_resource_bind_single_image_vram_slot` (`0x800444e4`), `image_bundle_load_to_vram` (`0x80044614`), `sprite_rle_decode_rows` (`0x80045264`), and frame-geometry helpers (`0x80045014`, `0x800450a8`, `0x8004513c`, `0x800451d0`). - Kind-4 decode lane is now extractor-explicit: single-image descriptors bind one VRAM slot and preserve descriptor width/height/format/payload metadata in the runtime resource; this is the minimal schema a standalone decoder must mirror before frame submission. - Kind-5 decode lane is now exporter-explicit: bundle install builds a runtime frame table (`0x10` stride), allocates frame VRAM slots, and uploads either raw rows or RLE-decoded rows depending on frame flag bit0. - RLE semantics are now pinned in live comments: positive control repeats a byte value, negative control copies literal run bytes, and zero terminates each row. This is the required offline decode contract for compressed WDL image payloads. - Geometry extraction rule is now explicit for offline export: width/height/origin come from kind-specific frame schemas (`0x14` stride kind-4 descriptor rows versus `0x10` stride kind-5 runtime rows), not from visibility-lane flags. Conservative live-artifact updates applied in Ghidra for this pass: - Renames: - `0x80045440` -> `psx_ui_image_bundle_get_frame_payload` - `0x800455d4` -> `psx_ui_image_bundle_draw_temp_vram_frame` - `0x80045d78` -> `psx_ui_image_bundle_draw_temp_vram_frame_static` - Decompiler comments: - `0x800444e4` (kind-4 single-image bind semantics) - `0x80044614` (kind-5 bundle upload table and raw-vs-RLE branch) - `0x80045264` (row-RLE decode contract for offline tools) - `0x80045014`, `0x800450a8`, `0x8004513c`, `0x800451d0` (kind-split frame geometry schema for exporter parity) ## Live MCP Follow-Up (2026-04-13): Loader Bundle Install, Stream Runtime Banks, and Inflate Lane - Scope for this focused pass: `wdl_resource_bundle_load_by_index` (`0x80039444`), `psx_stream_install_type_runtime_banks` (`0x80038f18`), `psx_install_type_state_script_component_extents_banks` (`0x8003917c`), `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`), detached-stream installer (`0x80040768`), and compressed-state inflate (`0x8003b00c`). - `psx_stream_install_type_runtime_banks` now has exporter-relevant record layout closure: each streamed type entry begins with a fixed `0x14` header `(type_id, state_size, component_size, extents_size, active_art_header_size)` before payload bytes. - Stream-lane install behavior is now role-split and explicit: - installs state/script, component, and extents pointers into `DAT_800758cc/d0/d4` - clears `DAT_800758c8[type]` (`psx_type_art_built_resource_bank`) to null - installs only raw active-header pointer into `DAT_800758d8[type]` when `active_art_header_size != 0`. - Practical consequence for standalone exporters: stream-runtime-bank lane seeds metadata/header pointers only; built drawable resources are resolved in the separate WDL art installer (`psx_install_type_art_active_header_and_built_resource`) through kind-4 bind and kind-5 image-table build. - `wdl_resource_bundle_load_by_index` is now preserved as a multi-pass install sequence with two art/state waves, section-pack pointer install, detached runtime-stream install, optional compressed-state inflate, and only then root-record dispatch. - Compressed-state lane remains pre-dispatch and persistent-state-gated: `psx_lzss_unpack_into_level_buffer` inflates a `0x3e00` block into `psx_level_decompressed_state_buffer` before runtime-header apply and root replay; a zero backref token terminates decode. - Detached runtime-stream blob install (`0x80040768`) is now extraction-structured: header carries three leading lengths, then two 9-entry size arrays, then tail-transfer length used for SPU upload and sequence/VAB setup. Conservative live-artifact updates applied in Ghidra for this pass: - Renames: - `0x8002b6b8` -> `psx_level_heap_push_cursor_mark` - `0x8002b6e0` -> `psx_level_heap_pop_cursor_mark` - `0x80024720` -> `psx_level_runtime_node_pool_init_0x32` - Decompiler comments: - `0x80038f18` (stream runtime-bank install role split and built-resource clear) - `0x80040768` (detached runtime-stream blob format/install semantics) - `0x8003b00c` (compressed-state inflate role, size lane, and termination) - `0x80024720` (0x32-node cyclic runtime-node pool initialization) ## Live MCP Follow-Up (2026-04-13): Late Art-Bank Dual Feed and Constructor `0x58` Raw-Header Fast Path - Scope for this focused pass: `wdl_resource_bundle_load_by_index` (`0x80039444`), late write sites `0x8003977c` / `0x80039a64`, `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`), `psx_create_image_resource_from_descriptor` (`0x80044434`), constructors `0x80024ab4` / `0x80024fac`, and sibling packed-stream helper `psx_stream_install_type_runtime_banks` (`0x80038f18`). - `wdl_resource_bundle_load_by_index` does not leave `DAT_800758d8` from one monolithic late art bank. For each WDL pass (`SPEC_A.WDL` first, selected `LSET*.WDL` second), it performs two distinct art-facing installs: - an earlier art-install blob (`header` field used as `local_a0`) that calls `psx_install_type_art_active_header_and_built_resource` - a later header-only override blob (`header` field used as `local_98`) whose `8`-byte rows write raw active-header pointers directly into `DAT_800758d8[type]` - The earlier art-install blob is now role-exact but still only medium confidence for raw standalone parsing: - `psx_install_type_art_active_header_and_built_resource` first stores the incoming header pointer to `DAT_800758d8[type]` - it then resolves kind `4` versus kind `5` resource build and writes the materialized resource to `DAT_800758c8[type]` - it finally mirrors that built resource pointer back into `DAT_800758d8[type]` - practical consequence: this pass is an install/build lane, not the final raw-header state seen by constructors after load completes - The later header-only override blob is the safer standalone parsing target and now has a tighter in-memory schema: - blob header begins with `count` and a directory offset - payload base is `blob + 0x08` - directory rows are `8` bytes each and are consumed as `(active_header_size, type_id)` - when `active_header_size != 0`, loader stores the current payload cursor to `DAT_800758d8[type]` - when `active_header_size == 0`, loader clears `DAT_800758d8[type]` - payload cursor then advances by `active_header_size` - Constructor-side reuse closure is now disassembly-backed and corrects an older decompiler-shaped reading: both constructors branch on `*(DAT_800758d8[type]) == 0x58`, not on `kind == 5`. - `0x80024b0c`: simple constructor raw-header fast path - `0x80025004`: compound constructor raw-header fast path - when the first dword is `0x58`, constructors treat `DAT_800758d8[type]` as a raw active header and reuse `DAT_800758c8[type]` instead of calling `psx_create_image_resource_from_descriptor` - `psx_create_image_resource_from_descriptor` remains the per-instance fallback builder for descriptors that do not arrive on that raw-header fast path. Its role did not change, but the exact condition for constructor reuse is now narrower and stronger. - `psx_stream_install_type_runtime_banks` still matters as a sibling negative-evidence lane: it proves there is a separate packed per-type bank format with a fixed `0x14` entry header `(type_id, state_size, component_size, extents_size, active_art_header_size)`. That stream format should not be conflated with the later `8`-byte header-only override blob used by `wdl_resource_bundle_load_by_index`. Candidate standalone raw-schema read after this pass: - High confidence: the late header-only override blob is the final post-load source that leaves raw `0x58`-byte active headers in `DAT_800758d8`, and its row walk is `8`-byte `(size,type)` with payloads packed immediately after the blob header. - Medium confidence: those `0x58`-byte active headers are the right standalone parser target for executable-faithful direct art binding because constructors discriminate on `0x58` and then reuse `DAT_800758c8`. - Low to medium confidence: the earlier art-install blob also feeds the same type lane, but its raw on-disk row encoding is still unresolved for standalone parsing because the runtime walk consumes pointer-like aux entries without an intervening relocation helper. Conservative live-artifact updates applied in Ghidra for this pass: - Decompiler comments: - `0x8003977c` (late header-only override writes raw active-header pointers) - `0x80039a64` (map-local repeat of the same override lane) - `0x800460d4` (built-resource mirror into `DAT_800758d8` is later overwritten by header-only pass) - `0x80024b0c` (simple constructor `0x58` raw-header fast path) - `0x80025004` (compound constructor `0x58` raw-header fast path) ## Live MCP Follow-Up (2026-04-13): Visibility Routing, Stage Lane Choice, and Main-Visible Ordering - Scope for this focused pass: active PSX `SLUS_002.68` around `psx_object_integrate_motion_and_route_visible` (`0x800131a8`), neighboring player/object update corridor (`0x8001263c..0x80013688`), and main-visible ordering helpers centered on `0x8002be6c`, `0x8002c89c`, `0x8002ca74`, `0x8002d778`, and `0x8002e064`. - Route split is now pinned as object-local in executable terms: `psx_object_integrate_motion_and_route_visible` chooses stage-2 only when `type==4` or `obj+0x1c` has bit `0x0400`; otherwise it remains in stage-1 main-visible projection/sort lane. - In the same function, policy table reads (`DAT_800675f8[type]`) are downstream gating and ordering controls, not lane selectors. The key stage route decision remains the object-local `0x0400` branch. - Main-visible ordering is now graph-explicit: `psx_main_visible_order_compare_pair_for_graph` computes relation codes from projected extents and policy bits, `psx_main_visible_order_graph_link_new_object` records edges, and `psx_main_visible_list_sort_range` resolves a dependency-eligible ordered slice while unlinking conflicts. - Stage-1 ordering is therefore not a simple distance sort. It is a dependency-graph sort with policy-biased tie resolution, and that graph order feeds the main-visible draw lane. Conservative live-artifact updates applied in Ghidra for this pass: - Renames: - `0x8002cd60` -> `psx_main_visible_order_graph_release_node` - `0x8002ce38` -> `psx_main_visible_order_graph_compact_nodes` - `0x8002cf3c` -> `psx_main_visible_order_graph_reset_node_slot` - `0x8002cfb0` -> `psx_main_visible_order_graph_swap_node_slots` - Decompiler comments: - `0x800131a8` (stage-1 vs stage-2 route split for exporter lane choice) - `0x8002be6c` (pair-order relation semantics and policy bit role) - `0x8002d778` (dependency-aware main-visible sort behavior) - `0x8002cd60` (node release semantics) - `0x8002ce38` (node compaction/prune semantics) - `0x8002cf3c` (node slot reset semantics) - `0x8002cfb0` (node slot swap semantics during compaction) Exporter-facing implication from executable evidence: - A standalone JS exporter should keep route lane and order phase separate in diagnostics and matching: - lane selection key: `type==4 || (obj+0x1c & 0x0400)` - stage-1 ordering key family: graph relation codes plus policy-adjusted compare/unlink behavior - Flattening stage-1 order to simple Y/depth or ignoring graph edge pruning will diverge from executable main-visible draw order even when lane choice is correct. ## Live MCP Follow-Up (2026-04-13): Kind-4/Kind-5 Palette Selection Field Closure - Scope for this focused pass: active PSX `SLUS_002.68` around `psx_resource_bind_single_image_vram_slot` (`0x800444e4`), `image_bundle_load_to_vram` (`0x80044614`), and submitters `psx_sprite_resource_submit_frame` (`0x80044bdc`) / `psx_image_table_submit_frame` (`0x80044e9c`) with lane callers in `psx_draw_main_visible_object` (`0x80041458`) and `psx_draw_special_visible_queue` (`0x80041144`). - Default CLUT source for both kind-4 and kind-5 remains resource field `resource+0x08`, seeded from descriptor/bundle header `+0x14` during bind. - No additional per-frame resource header field in the `0x800444e4/0x80044614` bind corridor directly selects CLUT index at submit time; frame-table records in this corridor drive geometry/payload upload, not palette-bank selection. - One additional resource header field does materially affect palette-routing semantics: resource format (`resource+0x04`, sourced from header `+0x10`) changes sprite override behavior. - format `== 2`: override token uses `psx_clut_override_table_by_palette_token[token]` directly. - format `!= 2`: override token is remapped as a bank-table row key in `psx_clut_table_by_resource_bank` (`(token<<4)` halfword lane). - Submit high-byte token source remains draw-lane/object-authored, not resource-header-local: - main-visible may inject authored high byte (`source+0x06` for `0x003e..0x00ab`, `source+0x0c` for `>=0x00ac`). - special-visible does not inject authored high byte. Conservative live-artifact updates applied in Ghidra for this pass: - Decompiler comments: - `0x800444e4` (kind-4 default palette-bank seed from header `+0x14`) - `0x80044614` (kind-5 default palette-bank seed and format-2 `+0x10` offset behavior) - `0x80044bdc` (sprite submit default/override CLUT resolution split) - `0x80044e9c` (image-table submit default/override CLUT resolution) Exporter-facing implication from executable evidence: - Standalone palette selection should treat header `+0x14` as the default bank key only. - Additional CLUT selection must come from runtime submit flags (lane/object authored token), with sprite format-aware override routing. - Do not infer palette index from frame-table width/height/origin/offset fields in the bind/upload helpers; those fields are geometry/payload metadata, not CLUT selectors.