407 lines
No EOL
38 KiB
Markdown
407 lines
No EOL
38 KiB
Markdown
# 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. |