38 KiB
PSX Art-Binding Recovery
Scope
- Active target: retail PlayStation
SLUS_002.68feeding the renderer-local cache pipeline inCrusader-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-remorseset carried58,262fallback placeholders versus only1,714bundle-mapped items. - The fallback concentration was overwhelmingly in
section0_constructor_placements(94.5%of fallback items), not in the smallersection0_dispatch_rootsfamily. - The hottest unresolved types were
0x0042,0x0041,0x0047,0x0045,0x003f,0x004b,0x0046,0x0044,0x0048, and0x004a. - Those types already appeared in the exported
DAT_800758d8art-template layer in many maps, but almost all of their rows wereblockSize = 0and therefore had no direct payload dwords to match against bundle offsets. - The neighboring state banks were still populated:
DAT_800758cccarried valid script tables for the same types, whileDAT_800758d0stayed empty for this family andDAT_800758d4remained 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:
- Use the direct non-zero
DAT_800758d8row when a type has a real local template payload and bundle match. - If the type lives in the unresolved zero-block constructor-placement family, look for a same-map donor type whose
DAT_800758ccscript blob is identical and whoseDAT_800758d8row resolves to a real bundle. - 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:
mappingSourcenow records whether the art came from a direct template, acc-signature-donor:xxxx, or ageneric-family-donor:xxxxpath.templateTypeIdanddonorTypeIdare preserved in exportedmapSourcerows.- The unresolved type still uses its own
DAT_800758ccselector-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,262fallback /1,714bundle-mapped to37,054fallback /22,922bundle-mapped.
Representative maps:
map 0:1078 / 111->340 / 849map 9:664 / 111->197 / 578map 43:707 / 97->196 / 608map 64:1194 / 161->736 / 619map 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,038fallback /34,938bundle-mapped.
Representative maps after the extended pass:
map 0:66fallback /1123bundle-mappedmap 9:42fallback /733bundle-mappedmap 43:9fallback /795bundle-mappedmap 64:160fallback /1195bundle-mappedmap 104:866fallback /136bundle-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
u5lane before the older coarse type default is consulted. - The exporter still prefers direct local
DAT_800758d8payload matches first, but when a type bucket is mixed-role or otherwise unresolved it now resolves each cohort separately instead of collapsing the wholemap:typebucket into one donor or one placeholder. - Focused validation on
map 104now rebuilds as1002real-art items with0fallback items,1atlas, and136shape definitions. ScenesourceCountsare now52section0_dispatch_roots-artplus950section0_constructor_placements-art. - The scene JSON also now preserves
mappingSourceand optionalartCohorton eachmapSourcerow, 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-*oremergency-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,038fallback items remain across62maps. - 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, and0x0063. 0x0042remains the single largest unresolved type even after the heuristic pass.map 104is the most obvious current outlier: it still sits at866fallback items versus only136bundle-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 1bundle header palette index+0x14as 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
16adjacent16-colorCLUTs flattened into one 256-entry palette. That is the rule now encoded inmap_renderer/src/lib/psx-cache.jsformode 1bundles. - 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 2stays on the earlier per-bundle/per-usage palette-index path. This lock-in applies specifically to the currentmode 1family 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 indexas a fallback rule.
Static Export Follow-Up
- The processed PSX catalog already carried
62maps during this pass, so the "single map" symptom was not a cache-build enumeration failure. - The immediate export-side blocker was config:
psx-remorsewas excluded from static export even though the prebuilt catalog/build-manager path already supports multi-map PSX scenes. - The renderer config now includes
psx-remorsein 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 inpsx_lset_world_frame_wrapper(0x80031f0c) andpsx_draw_world_visible_passes(0x80041378), with submitter override gates at0x80044e10and0x80044eb8. - Main-visible now has instruction-level confirmation for flag packing at the call sites (
0x800415c0/0x800415e0):a3 = (obj_flags & 0x0002) | token_hitoken_hiis normalized earlier assource_palette_word & 0xFF00at0x80041590.
- Exact palette-token carriage for this lane is therefore bits
15:8of 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&0x0002plus token high byte, bit0x0002alone does not trigger override; nonzero token high byte is the effective palette-override activator.
- gate expression is
- Non-obvious normalization outcome for exporter logic: no additional wrapper-stage or no-op-hook mutation exists before
0x80041458submits; 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_step0x80038f10 -> psx_noop_frame_hook_38f100x80044018 -> 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.68world draw and submitter routing at0x80041378,0x80041458,0x80041144,0x80044bdc, and0x80044e9c, plus CLUT table lanes0x800a9f48and0x800a9f66. - 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==5image-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 selectspsx_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_860x8002eee8 -> 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.68around 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) andpsx_object_create_compound_record(0x80024eec) read the per-type active art header fromDAT_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+0x9eand script cursorobj+0x90), thenpsx_object_advance_state_script(0x80025d68) latches live frame tokenobj+0x94from 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 fromobj+0x94, and submitters read the same token inpsx_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 clarifyingDAT_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 consumesobj+0x94but 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, and0x80044e9c, with CLUT table symbols at0x800a9f48and0x800a9f66. 0x800a9f48remainspsx_clut_table_by_resource_bankand is both load-populated (level_palette_upload_cluts) and draw-consumed.0x800a9f66remainspsx_clut_override_table_by_palette_tokenand 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 forwardsobj_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
0behaves as "no override": with only low flag bits present, submitters fall through to default bank CLUT lookup. psx_image_table_submit_frameuses token as a key intopsx_clut_override_table_by_palette_token[token]when override is active.psx_sprite_resource_submit_framehas two override lanes based on resource format field (resource+4):- format
== 2: token keyspsx_clut_override_table_by_palette_token[token]. - format
!= 2: token is used as a row key intopsx_clut_table_by_resource_bank(token << 4halfword index).
- format
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 around0x8002906c. psx_object_select_state_scriptis confirmed as install-only in exporter terms: it writes selectorobj+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_scriptwhere current script word is copied toobj+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 frompsx_type_transition_selector_rows(0x80063b4c), thenpsx_object_select_state_scriptinstalls selector. - In that transition path, runtime flag mutation is narrow and explicit: selector logic toggles
obj+0x1cbit0x0002only; broad authored lane bits such as0x0020are not synthesized by this function. psx_type42_transition_selector_tickadds an early pre-latch gate before reseat/turn-driven selector dispatch: object must be within view margin and pass the object-laneobj+0x1c & 0x0020condition. Selector updates there still occur before the laterobj+0x94latch point.- The unresolved
FUN_8002906cpath is now closed by symbol state in live Ghidra:0x8002906cispsx_type4_reselect_motion_state, reached frompsx_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-
0x0042reselection 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+0x94capture (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 directobj+0x94write.0x80025d68: comment clarifying final frame/state latch intoobj+0x94.0x8001bca0: comment clarifying two-stage transition-row lookup and0x0002-only bit toggle scope.0x80018578: comment clarifying type-0x0042pre-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.68atpsx_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 row0x800626f8. - 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 row0x800626f8callback0x80013618. - Descriptor row role at
0x800626f8is 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)
- slot0
- 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.
- simple path copies authored route word
- 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_80031c34renamed topsx_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.68constructor/source-pointer storage and world main-visible token read at0x80024b50,0x80025048,0x800258cc, and0x80041458. - 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
- for
- Constructor storage of
obj+0xa0is now instruction-explicit in both create paths:- simple record path writes source pointer at
0x80024b50 - compound/region00 path writes source pointer at
0x80025048
- simple record path writes source pointer at
- 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+0x06only; 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+0x0cread requires a longer source layout.
Conservative live-artifact updates applied in Ghidra for this pass:
0x80027f38:FUN_80027f38renamed topsx_alloc_runtime_snapshot_record_payload.0x80024b50: decompiler comment clarifying simple-pathobj+0xa0source-record pointer storage and later main-visible+0x06/+0x0ctoken reads.0x80025048: decompiler comment clarifying compound-pathobj+0xa0storage and 12-byte-record compatibility with<0x00ac+0x06token read.0x8004156c: decompiler comment clarifying the exact type-band split and source offsets used for main-visible palette-token injection.
Next Steps
- Trace the remaining high-volume band
0x0055..0x0063in Ghidra with the same question used for0x0042: why doesDAT_800758d8stay zero-sized while visible art still exists at runtime? - Use
map 104as the primary regression target and dump its remaining fallback type/state/lane distribution before doing any broader heuristic expansion. - 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.
- Keep the
DAT_800758d4work 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.68type-4/type-5 install and submit corridor centered onpsx_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 (
0x10stride), 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 (
0x14stride kind-4 descriptor rows versus0x10stride 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_payload0x800455d4->psx_ui_image_bundle_draw_temp_vram_frame0x80045d78->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_banksnow has exporter-relevant record layout closure: each streamed type entry begins with a fixed0x14header(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]whenactive_art_header_size != 0.
- installs state/script, component, and extents pointers into
- 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_indexis 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_bufferinflates a0x3e00block intopsx_level_decompressed_state_bufferbefore 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_mark0x8002b6e0->psx_level_heap_pop_cursor_mark0x80024720->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 sites0x8003977c/0x80039a64,psx_install_type_art_active_header_and_built_resource(0x80045ffc),psx_create_image_resource_from_descriptor(0x80044434), constructors0x80024ab4/0x80024fac, and sibling packed-stream helperpsx_stream_install_type_runtime_banks(0x80038f18). wdl_resource_bundle_load_by_indexdoes not leaveDAT_800758d8from one monolithic late art bank. For each WDL pass (SPEC_A.WDLfirst, selectedLSET*.WDLsecond), it performs two distinct art-facing installs:- an earlier art-install blob (
headerfield used aslocal_a0) that callspsx_install_type_art_active_header_and_built_resource - a later header-only override blob (
headerfield used aslocal_98) whose8-byte rows write raw active-header pointers directly intoDAT_800758d8[type]
- an earlier art-install blob (
- 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_resourcefirst stores the incoming header pointer toDAT_800758d8[type]- it then resolves kind
4versus kind5resource build and writes the materialized resource toDAT_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
countand a directory offset - payload base is
blob + 0x08 - directory rows are
8bytes each and are consumed as(active_header_size, type_id) - when
active_header_size != 0, loader stores the current payload cursor toDAT_800758d8[type] - when
active_header_size == 0, loader clearsDAT_800758d8[type] - payload cursor then advances by
active_header_size
- blob header begins with
- Constructor-side reuse closure is now disassembly-backed and corrects an older decompiler-shaped reading: both constructors branch on
*(DAT_800758d8[type]) == 0x58, not onkind == 5.0x80024b0c: simple constructor raw-header fast path0x80025004: compound constructor raw-header fast path- when the first dword is
0x58, constructors treatDAT_800758d8[type]as a raw active header and reuseDAT_800758c8[type]instead of callingpsx_create_image_resource_from_descriptor
psx_create_image_resource_from_descriptorremains 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_banksstill matters as a sibling negative-evidence lane: it proves there is a separate packed per-type bank format with a fixed0x14entry header(type_id, state_size, component_size, extents_size, active_art_header_size). That stream format should not be conflated with the later8-byte header-only override blob used bywdl_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 inDAT_800758d8, and its row walk is8-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 on0x58and then reuseDAT_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 intoDAT_800758d8is later overwritten by header-only pass)0x80024b0c(simple constructor0x58raw-header fast path)0x80025004(compound constructor0x58raw-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.68aroundpsx_object_integrate_motion_and_route_visible(0x800131a8), neighboring player/object update corridor (0x8001263c..0x80013688), and main-visible ordering helpers centered on0x8002be6c,0x8002c89c,0x8002ca74,0x8002d778, and0x8002e064. - Route split is now pinned as object-local in executable terms:
psx_object_integrate_motion_and_route_visiblechooses stage-2 only whentype==4orobj+0x1chas bit0x0400; 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-local0x0400branch. - Main-visible ordering is now graph-explicit:
psx_main_visible_order_compare_pair_for_graphcomputes relation codes from projected extents and policy bits,psx_main_visible_order_graph_link_new_objectrecords edges, andpsx_main_visible_list_sort_rangeresolves 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_node0x8002ce38->psx_main_visible_order_graph_compact_nodes0x8002cf3c->psx_main_visible_order_graph_reset_node_slot0x8002cfb0->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
- lane selection key:
- 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.68aroundpsx_resource_bind_single_image_vram_slot(0x800444e4),image_bundle_load_to_vram(0x80044614), and submitterspsx_sprite_resource_submit_frame(0x80044bdc) /psx_image_table_submit_frame(0x80044e9c) with lane callers inpsx_draw_main_visible_object(0x80041458) andpsx_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+0x14during bind. - No additional per-frame resource header field in the
0x800444e4/0x80044614bind 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 usespsx_clut_override_table_by_palette_token[token]directly. - format
!= 2: override token is remapped as a bank-table row key inpsx_clut_table_by_resource_bank((token<<4)halfword lane).
- format
- Submit high-byte token source remains draw-lane/object-authored, not resource-header-local:
- main-visible may inject authored high byte (
source+0x06for0x003e..0x00ab,source+0x0cfor>=0x00ac). - special-visible does not inject authored high byte.
- main-visible may 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+0x10offset 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
+0x14as 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.