- 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.
- 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.
- 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.”
- 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.
- 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:
## 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.
## 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.
-`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:
- 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`.
## 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.
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:
## 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:
-`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:
- 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)
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.