PSX Research

This commit is contained in:
Marco 2026-04-07 17:16:44 +02:00
commit 94c49ac5bd
16 changed files with 2052 additions and 16 deletions

View file

@ -571,7 +571,7 @@ Immediate implications for the next decode pass:
- the public renderer integration path is now proven enough to use as a live debug target for PSX map-format work
- the next priority is to replace the invalidated `u0 -> bundle index` hypothesis with a real type/resource lookup recovered from `level_resource_stream_load`
- `post_audio_region_00` is now a top-tier candidate for that work because its new diagnostics expose a count-prefixed preamble plus compact typed records that look more loader-compatible than the old region-01 art probe
- the palette override path is still the main blocker to correct final color selection even when the bundle/frame choice is plausible
- the palette override path is now partially landed in the viewer/exporter too: the cache builder applies the executable-backed authored override byte when the source record exposes the proven `+0x06` / `+0x0c` lane, so the remaining blocker is the cases where the runtime first picks a different object/variant/state than the current export model
- once the bundle key and palette control path are recovered, the same scene-export path can graduate from `real-art probe` to actual PSX map rendering
## PSX Provisional Real-Art Probe
@ -632,7 +632,19 @@ The current live viewer export was built on the wrong premise. The `~45..59` rec
What the loader actually does:
- `wdl_resource_bundle_load_by_index` reads the selected `LSET*.WDL` into multiple section pointers, not one flat placement stream.
- the per-level file header is now structurally tighter too: after the initial `0x38` read, the loader treats the first nine dwords as contiguous section sizes, allocates a separate `0x50` runtime-header block at `DAT_80067794`, and later reads one more independently allocated compressed source blob before the optional `FUN_8003b00c(..., 0x3e00, 0x3e00)` inflate.
- The first runtime section is a top-level table at `DAT_800678f4` whose record stride is `0x18` bytes.
- The second runtime section at `DAT_80067720` is also `0x18`-stride, but it is not just a duplicate alias of the first pointer. The loader assigns the contiguous level sections in this order:
- `DAT_800678f4 = section[0]` root dispatch table
- `DAT_80067720 = section[1]` secondary `0x18`-stride control/placement table
- `DAT_800678f0 = section[2]`
- `DAT_80067938 = section[3]`
- `DAT_80067838 = section[5]`
- `DAT_800675f8 = section[6]`
- `DAT_8006754c = section[7]`
- `DAT_80067840 = section[8]`
- `DAT_800676d8 = section[9]` palette block loaded just before `level_palette_header_apply`
- The skipped header slot is not dead space. The loader allocates it separately as `DAT_8006767c`, then feeds it to `FUN_80040768`; that lane stays distinct from the contiguous section pack and from the later `DAT_8006b5d8 -> DAT_8006769c` decompression path.
- The loader iterates that first section with:
- `for each 0x18-byte top-level record`
- `type = record[+0x08]`
@ -684,7 +696,9 @@ Exporter status after the next renderer pass:
- `section0_constructor_placements` for the dense constructor-fed placement records
- a verified full rebuild now exports all `62` PSX maps with large scene volumes and non-flat `z` stats. `LSET1/L0.WDL` now emits `1189` items, `LSET1/L1.WDL` jumps from `53` items to `754`, and the rebuilt catalog reports `62/62` maps with `section0_dispatch_roots + section0_constructor_placements` coverage and `uniqueZCount > 1`.
- the renderer-side reference payload no longer emits one atlas per resolved PSX shape. The new packed-atlas pass reduces the shared PSX reference cache from the old `4032` one-shape atlases to `1925` shared packed atlases across the same `4032` shape definitions, and a spot-check on `LSET1/L0.WDL` now exports the map scene itself with `atlasCount = 1` instead of a long per-bundle atlas list.
- the cache export still carries the parsed `DAT_800758d8` candidate section and an offline `FUN_8003b00c` decode candidate for the compressed source feeding `DAT_8006b5d8 -> DAT_8006769c`, but the generic raw-file `DAT_800758cc/d0/d4` serialization is not currently landing in the live scene cache and should be treated as an open exporter gap rather than a closed layer.
- the cache export now carries more than the parsed `DAT_800758d8` candidate section. In the current `psx-runtime-record-probe-v6` scene path, `map_renderer/src/lib/psx-cache.js` serializes `DAT_800758cc`, `DAT_800758d0`, `DAT_800758d4`, and the offline `FUN_8003b00c` decode candidate for `DAT_8006b5d8 -> DAT_8006769c` into `stateLayers`, and the scene writer preserves those layers in both scene metadata and `mapSource`.
- the new typed-section-16 discovery path is also broader than the earlier section-start probe: when no parsed-section candidate wins, the cache builder now falls back to an absolute-file scan, which is why the late compound-bank blobs can now land in the export even when their serialized source does not start exactly at a pre-labeled section boundary.
- the file-side header block now separates more cleanly too: `FUN_80039c40` allocates a `0x50` level runtime-header block at `DAT_80067794`, and `FUN_80039dc4` copies that block into globals such as `DAT_80078ab0`, `DAT_80078a88`, `DAT_80078a8c`, `DAT_80078a4c`, and `DAT_80067354` before calling `FUN_80042ec4`. So the first visible/root sections are not the only authoritative level metadata; the loader also applies a dedicated `0x50` per-level runtime header after the optional `0x3e00` decode succeeds.
- this still does not mean the PSX map decode is fully solved: the viewer now has enough volume to represent whole-level candidates across the disc, but the remaining blocker is semantic decoding of the subordinate runtime banks and the separate decompressed `0x3e00` buffer, not record-count starvation.
- the type-to-art path is only partially improved. The cache builder now scans the parsed per-type art-template payloads for bundle references, and the renderer no longer treats the disproven scan-order `u0 -> bundle` mapping as trustworthy visible art. Unverified types now stay on placeholder art instead of surfacing known-bad portrait/talk bundles as map geometry.
- the scan-order fallback is now known to be wrong at the root, not merely incomplete. In the live `.cache` output, `section0_dispatch_roots` types `0x0042` and `0x0049` repeatedly bind to portrait/talk-animation bundles such as map `0` type `0042` -> offset `0x000B2970` and map `0` type `0049` -> offset `0x000D84F4`, with the same failure pattern continuing through early maps. Those portrait bundles are useful negative evidence: they show the top-level dispatch rows are generic object/state descriptors, not a direct map-graphics stream that can be paired to bundle order.
@ -695,6 +709,11 @@ Next decoded runtime layers from the constructor pass:
- `DAT_800758d0` is a per-type companion/component bank for the simpler constructor family. `FUN_800249f4` copies the resolved pointer from that bank into the local object payload at `obj->8->[0,4]`, so this looks like a per-type component/template block rather than a top-level placement stream.
- `DAT_800758cc` is a per-type offset-table bank for the compound constructor family. `FUN_80024eec` stores it at `obj+0x88`, and `FUN_800260e8` later indexes it with the placement byte at `record+0x08` to resolve a state/offset subrecord into `obj+0x8c/0x90`.
- `DAT_800758d4` is another per-type companion bank for the compound constructor family. `FUN_80024eec` stores it at `obj+0x84`, and `FUN_8002841c` queries it later using the object's `+0x94` selector, so it behaves like a variant table or companion lookup rather than raw map geometry.
- The raw-file provenance for those banks is tighter now too. Inside `wdl_resource_bundle_load_by_index`, `psx_load_type_state_banks` is called four times total: twice before the selected `LSET*.WDL` opens and twice again after the level-local section pack and palette/header lanes are loaded. The standalone `8`-byte descriptor-table read that assigns `DAT_800758d8` sits between the second pair of `psx_load_type_state_banks` calls. Current best read:
- one common/shared bank pair loads before the map-local WDL opens
- one map-local override pair loads after the level-local header and palette lanes
- `DAT_800758d8` definitely comes from its own late descriptor stream
- `DAT_800758d0` plus `DAT_800758cc/d4` are loaded through the adjacent `psx_load_type_state_banks` blobs rather than from the root section tables or the decompressed `0x3e00` state buffer
- The key functions in that chain are now renamed in the live PSX Ghidra database:
- `FUN_800249f4` -> `psx_object_create_simple_record`
- `FUN_80024eec` -> `psx_object_create_compound_record`
@ -711,16 +730,19 @@ Next decoded runtime layers from the constructor pass:
- `psx_create_image_resource_from_descriptor` turns the `DAT_800758d8` per-type descriptor into a renderable resource/header object; this is why `DAT_800758d8` should be read as an art/template descriptor bank, not as a whole-map tile layer.
- `psx_object_select_state_script` selects a state or animation subrecord from `DAT_800758cc` using a placement byte (`record+0x08` in the compound family), storing the resolved script/state pointer at `obj+0x8c/0x90` and the selector at `obj+0x9e`.
- `psx_object_advance_state_script` then interprets the active state script with sentinel/control values such as `0xffff`, `0xfffe`, `0xfffd`, `0xfffc`, and `0xfffb`, so the visible frame path is explicitly state-driven rather than just "type id -> one bundle".
- `psx_object_lookup_variant_entry` is not only a constructor-time helper. Its call graph now shows three direct consumers: `psx_object_create_simple_record`, `psx_object_create_compound_record`, and `psx_object_advance_state_script`. That means unresolved families cannot be modeled as one spawn-time `type -> variant` choice; the visible companion bytes can be recomputed after state-script control flow advances.
- The current renderer-side consequence is important: section-0 word `u4` is no longer treated as a verified sprite-frame index. It is now carried forward as a state-selector candidate in exported scene metadata until the `DAT_800758cc/d4` path is decoded far enough to pick the right animation frame from executable evidence.
- Current strongest sentinel read:
- `0xfffe` dispatches `FUN_8004061c`, which is an audio/effect helper rather than a visible-frame selector.
- `0xfffd` is an in-script jump/re-anchor control that rewrites `obj+0x90` relative to the current script base.
- `0xfffc` switches `obj+0x8c/0x90` to another subsidiary script selected through the `DAT_800758cc` offset table.
- `0xfffb` also switches into a subsidiary script, but first scans forward to an in-script `0xfffd` marker before choosing the destination entry.
- `0xfffc` resolves a fresh subsidiary script base from the table rooted at `obj+0x88`, then immediately swaps both `obj+0x8c` and `obj+0x90` to that destination before continuing from the first record there.
- `0xfffb` also resolves a subsidiary script from the same table, but first walks the current script forward until it finds an in-band `0xfffd` marker and then uses that marker's selector word to choose the destination entry.
- Current best read of those sentinels:
- `0xffff` marks a terminal or restart control that re-anchors the script at `obj+0x8c` and raises object-state flags.
- `0xfffe` dispatches a side-effect helper (`FUN_8004061c`) using the following word as a parameter before advancing.
- `0xfffd`, `0xfffc`, and `0xfffb` switch into subsidiary scripts through the `DAT_800758cc` offset table rooted at `obj+0x88`.
- `0xfffd` is the direct indexed jump control within the current script family.
- `0xfffc` and `0xfffb` are both subsidiary-script switches through the `DAT_800758cc` offset table rooted at `obj+0x88`, but `0xfffb` is specifically the scan-forward variant that consumes the next in-band `0xfffd` selector.
- because `psx_object_advance_state_script` calls `psx_object_lookup_variant_entry` after those control-flow steps, the visible art choice for unresolved types may depend on post-jump script state rather than on the placement selector byte alone.
- `psx_object_lookup_variant_entry` finally uses `obj+0x94` to look up a companion entry in `DAT_800758d4`, which means even after construction the art-facing choice is still mediated by per-type variant/state tables.
- This means the next PSX layers are now at least structurally separated:
- visible root descriptors (`section0_dispatch_roots`, legacy alias `region00`)
@ -731,9 +753,16 @@ Next decoded runtime layers from the constructor pass:
- per-type compound variant tables (`DAT_800758d4`)
- the still-separate decompressed `0x3e00` level-state buffer (`DAT_8006769c`)
- The current renderer pass now records those banks explicitly as exported scene/state layers, while still only rendering the first two as visible scene items.
- The live viewer now keeps those heavyweight bank dumps out of the hot interactive scene state after load: `stateLayers` and `decodedRuntimeLayers` are preserved for exported scene JSON, but the runtime pan/zoom/editor path uses a trimmed scene object so normal navigation does not keep dragging the full PSX research blobs through the controller.
- The renderer-side state parser is also stronger than the earlier flat selector map. `buildTypeStateFrameMaps` now reads the exported `DAT_800758cc` layer and follows the currently verified control-flow sentinels (`0xfffe`, `0xfffd`, `0xfffc`, `0xfffb`) instead of just taking the first word of each selector entry. That does not fully close type `0x0042` or `0x0049`, but it does mean the viewer can now distinguish more fallback states from real script structure rather than from a blind `selector -> first frame` heuristic.
- The live cache now tightens the selector provenance too. In exported PSX scene items, the `state_selector=...` label is written directly from raw word `u4`, while the trailing `lane=...` label is raw word `u5`; this is not a post-hoc heuristic. A full scan of the built `psx-remorse` scene cache found `3944` visible `type=0x0042` placeholders across `61` maps, and the observed selectors track `u4` exactly (`0 -> 0x0000`, `1 -> 0x0001`, `2 -> 0x0002`, `3 -> 0x0003`, `4 -> 0x0004`).
- The executable-side handoff is narrower than that label might suggest. `psx_object_select_state_script` stores the authored selector byte in `obj+0x9e` and uses it only to choose the initial `DAT_800758cc` script base at `obj+0x8c/0x90`; `psx_object_lookup_variant_entry` does not index `DAT_800758d4` by `obj+0x9e`. It indexes by `obj+0x94`, which `psx_object_advance_state_script` refreshes from the current script entry after control-flow handling.
- Immediate map-viewer consequence: the current fallback art probe should be treated only as a diagnostic overlay for candidate bundle families. A workable renderer will need to recover the per-type `DAT_800758d8` descriptor mapping and the downstream `DAT_800758cc/d4` state+variant selection path before it can decide whether a section-0 placement should show world geometry, an animated object, or something non-map-facing like a portrait/talk asset.
- Current status note: even with the recovered placement/projection path and the first subset of real bundle-backed types, the live PSX map output is still unreadable as a practical map because most section-0 placements still miss the executable's final state/variant-driven art binding and therefore collapse back to placeholders.
- The next loader-side correction is now verified in the live cache too: the effective late `LSET*.WDL` `DAT_800758d8` candidate is not the earlier small-section heuristic, but a large late section whose working descriptor stream begins at an embedded `+0x38` offset. On retail map `9` that correction alone lifts `bundleMappedItemCount` from `0` to `111`, which is enough to restore real bundle-backed art for a first subset of types without reintroducing the disproven scan-order fallback.
The still-unresolved root-dispatch families remain instructive rather than contradictory. `0x0042` and `0x0049` still stay on placeholders after the bank-selection fix, but the same pass now decodes their `DAT_800758cc` state rows more cleanly: type `0x0042` carries three selector-targeted scripts (`0`, `1`, `2`) that all terminate through `0xffff`, while type `0x0049` carries a single selector-`0` script. So the remaining blocker for those roots is no longer "find any late template bank at all"; it is the deeper `DAT_800758cc/d4` state-to-visible-art bridge.
The still-unresolved root-dispatch families remain instructive rather than contradictory. `0x0042` and `0x0049` still stay on placeholders after the bank-selection fix, but the same pass now decodes their `DAT_800758cc` state rows more cleanly: type `0x0042` carries three selector-targeted scripts (`0`, `1`, `2`) that all terminate through `0xffff`, while type `0x0049` carries a single selector-`0` script. The built scene cache shows that this is still not the whole art-facing discriminator: `type=0x0042` placeholders now appear with selectors `0..4`, and the higher selectors `3` and `4` are real exported cases rather than parser noise. Verified maps with `0x0042` selectors above `2` include `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`.
Two runtime reselection paths now explain how those higher selectors can arise without contradicting the earlier three-script file read. `FUN_80028c94` and `FUN_8002906c` both recompute the active script with `psx_object_select_state_script(obj, FUN_8003bc1c(obj) >> 2 & 0xf)`, where `FUN_8003bc1c` quantizes the object's current motion vector at `obj+0x60/+0x64` through `FUN_8003b980` into a 16-way heading bucket. So cache-visible `0x0042` selectors `3` and `4` can come from runtime heading/state reselection, not only from the original placement byte.
That cache sweep also separates selector from lane more clearly than before. `0x0042` appears heavily on lanes `0x0020` and `0x0022`, and there are also map-local `lane=0x0030` cases (for example large clusters on `map-108`) that still export `state_selector=0`. So the unresolved bridge is narrower now: the visible-art rule cannot be modeled as just `u5` or just the initial `DAT_800758cc` selector parse. The remaining unknown is the downstream interaction between `u4`/`obj+0x9e`, the active state-script pointer at `obj+0x8c/0x90`, and the `DAT_800758d4` companion lookup that reruns after state-script advancement.
A first renderer-safe bridge landed even with that exporter gap still open: the verified `0x0050` state-script mapping (`selector 0..3 -> frame 0..3`) is now applied as a narrow fallback in the cache builder, and the rebuilt live map-9 scene now shows `type=80 state_selector=1 chosen_frame=1` instead of the old forced `chosen_frame=0`. Unresolved fallback placeholders are also now clamped to `opacity=0.45` in live scene output so the still-missing families stop visually overpowering the recovered real art. This remains intentionally scoped: the fallback frame map only covers the one family with direct executable-backed frame evidence, and the opacity clamp is diagnostic relief rather than a decoding claim.
The current draw split is clearer too. `FUN_80041378` is a three-stage render pass:
- stage 2: a second special-visible list drawn by `FUN_80041144`
@ -764,6 +793,81 @@ Recovered next visible layer from the bulk placement family:
- The PSX cache builder now uses that recovered `z` byte for `section0_constructor_placements` projection instead of forcing the whole bulk layer onto `z = 0`, while the top-level `section0_dispatch_roots` descriptor stream stays at `z = 0` until its own constructor-backed height source is proven.
- This is now the first PSX export pass in the viewer pipeline that produces visibly multi-layer whole-map candidates across the rebuilt retail catalog from executable-backed height data rather than from a single flattened candidate layer.
Additional constructor-backed coordinate grounding from the current pass:
- the current constructor split is now precise enough to state both authored row layouts at once:
- `psx_object_create_compound_record` reads `type` from `record+0x00`, `x/y` from `u16` words at `+0x02/+0x04`, authored elevation from byte `+0x06`, flags from `+0x0a`, and the initial state selector from byte `+0x08`.
- `psx_object_create_simple_record` reads `type` from `record+0x04`, `x/y` from `u16` words at `+0x08/+0x0a`, authored elevation from byte `+0x0c`, and the initial state selector from byte `+0x0e`.
- both constructors write the original authored source-record pointer to object field `+0xa0`, which keeps the later palette-override and state-selection paths tied to raw placement bytes rather than to a hidden runtime copy.
- both constructors also resolve the same per-type banks before any render pass runs:
- `DAT_800758d8` -> art/template descriptor
- `DAT_800758cc` -> state-script offset table
- `DAT_800758d4` -> companion variant lookup
- `DAT_800758d0` -> simple-record-only component payload
- this strengthens the viewer/exporter rule again: the two visible section-0 row families are constructor inputs, not already-final render primitives. Any direct `type -> bundle/frame` export path still has to respect the later state-script and variant lookups.
Recovered per-level runtime-header lane:
- `FUN_80039c40` is now confirmed as a pure `0x50` allocator for `DAT_80067794`, and `FUN_80039dc4` is the matching applier for that block.
- `FUN_80039dc4` copies fixed fields from `DAT_80067794` into the active level globals, including camera/runtime anchor values and several per-level mode bytes, then calls `FUN_80042ec4` to refresh dependent runtime state.
- The downstream call graph narrows that lane further: `psx_apply_level_runtime_header_block` is the only loader-side caller that feeds those values from WDL data, while `FUN_80042ec4` is also reused by `psx_input_device_init`, `memory_card_menu_tick`, and one additional front-end/system path. Current safest read: `DAT_80067794` is shared per-level runtime mode or presentation state rather than hidden bulk map geometry.
- Practical exporter consequence: keep the `DAT_80067794` fields as a first-class raw metadata lane, but do not treat them as a missing placement stream. They are more likely to affect camera/runtime modes, screen-space behavior, or level-global toggles than to supply extra map cells directly.
- The higher-level level lifecycle is now readable too. `psx_level_session_loop` is the outer level-session loop: it loads the selected WDL through `wdl_resource_bundle_load_by_index`, applies shared overlay/resource setup through `FUN_800388a8`, resets a small per-level step-flag block with `FUN_8003a498`, and then runs `psx_world_frame_tick` as the per-frame world loop until the current level session exits.
- `wdl_resource_bundle_load_by_index` is now mapped tightly enough for viewer work. Its effective order is: load `SPEC_A.WDL` and shared type art/state banks; open the selected `LSET*.WDL`; read the `0x38` section-size header; lay out the contiguous per-level section pack at `DAT_800678f4`, `DAT_80067720`, `DAT_800678f0`, `DAT_80067938`, `DAT_80067838`, `DAT_800675f8`, `DAT_8006754c`, `DAT_80067840`, and `DAT_800676d8`; load the detached `DAT_8006767c` blob; optionally inflate `DAT_8006b5d8` into `DAT_8006769c`; apply the runtime header at `DAT_80067794`; then dispatch the `0x18`-stride root records at `DAT_800678f4` through the per-type function table in `PTR_PTR_80063118`.
- The per-frame world loop in `psx_world_frame_tick` is now split clearly enough for renderer planning. In the normal in-level branch it ticks existing live objects through `psx_run_live_object_type_updates`, instantiates or refreshes nearby authored records through `psx_dispatch_section0_dispatch_roots` and `psx_dispatch_section0_constructor_placements`, runs per-object behavior callbacks through `psx_run_live_object_behavior_callbacks`, integrates world/player motion and active-object state through `FUN_80029de0`, updates queued transient resources through `FUN_8002aed0`, and only then submits the draw pass through `FUN_80041378`.
- The two authored record-family passes now line up directly with the viewer exporter model:
- `psx_dispatch_section0_dispatch_roots` walks the `DAT_80067720` `0x18`-stride family plus the fixed-size entries at `DAT_80067658`, culls them to roughly a `+/-0x140` neighborhood around the current focus object, and dispatches their per-type handlers. This is the closest executable match for the current `section0_dispatch_roots` viewer family.
- `psx_dispatch_section0_constructor_placements` walks the `DAT_800678f0` `0x0c`-stride family with the same neighborhood cull and per-type dispatch. This is the closest executable match for the current `section0_constructor_placements` viewer family.
- The already-instantiated-object passes are separated too:
- `psx_run_live_object_type_updates` iterates the linked live object list at `DAT_800675ac` and calls the per-type update callback (`type_vtable+8`) for active in-world objects.
- `psx_run_live_object_behavior_callbacks` then runs each live object's callback stored at `obj+0x98` / `obj[0x26]`, which is the later object-specific behavior/update pass.
- `FUN_80029de0` is the broad world-motion and player-state integrator that sits between behavior updates and draw submission. For viewer purposes, this is the runtime bridge between authored map placement and the motion/state values that later feed heading-based state reselection and projection.
- The cull-to-draw bridge is now closed too. `FUN_800423b0` is the authored-record screen-space gate used by the two section-0 dispatch passes, while `FUN_80042424` is the corresponding gate for already-instantiated live objects. Both use the same isometric camera basis around `DAT_800678d4`, which means the viewer can treat the record-family export as feeding the same projection space as the later live object list instead of as a separate map coordinate model.
- The main world-object draw helper is now grounded more tightly as well. `FUN_80041458` builds the final sprite primitive from the authored screen rectangle at `obj+0x20..+0x2e`, then ORs in a palette override read from the original source-record pointer at `obj+0xa0`: for types `0x003e..0x00ab` it uses the high byte of source word `+0x06`, and for types `>= 0x00ac` it uses the high byte of source word `+0x0c`. That means the remaining viewer mismatch is not where the override comes from, but when the runtime chooses a different object/variant/state before draw.
- The stage split is tighter too. `psx_project_object_special_visible_queue` (`0x80040f78`) feeds a distinct world-facing stage-2 queue, and `FUN_80041144` consumes that queue with the same projected screen rectangle fields and the same resource-specific draw helpers used by the stage-1 visible list. So the unreadable output is not explained by one missing HUD lane; the dominant gap is still the unresolved final art-binding path, with the stage-2 queue as a secondary world-object lane the viewer must eventually model.
- The next high-value executable target is now partly closed. `FUN_8002906c` is now renamed `psx_type4_reselect_motion_state`, and the surrounding interaction cluster is finally concrete enough to describe instead of leaving it as a black box:
- `psx_type4_update_delayed_interaction` (`0x80029c20`) is the type-4-only delayed wrapper. It probes ahead, stores the hit object at the controller-local `+0x38` slot, seeds a countdown from distance and speed, and dispatches to `psx_type4_reselect_motion_state` when that delay matures.
- `psx_type4_reselect_motion_state` (`0x8002906c`) is the post-construction reselection path for those delayed type-4 interactions. Depending on target flags it either hands off to the older `psx_object_reselect_state_from_target_vector` (`0x80028c94`) helper or flips the object's motion components against the target bounds, then reseats the live state script through `psx_object_select_state_script(obj, psx_object_quantize_motion_heading16(obj) >> 2 & 0xf)` before registering bilateral contact.
- `psx_object_update_nearby_interactions` (`0x80029478`) is the broad nearby-object sweep that feeds most of the non-type-4 collision and interaction bookkeeping. It walks the active object set, culls locally, performs overlap checks, updates directional contact/block flags, and registers contact pairs.
- `psx_object_test_overlap_3d` (`0x80028298`) is the box-overlap predicate used by that sweep, and `psx_object_update_contact_block_flags` (`0x800289f0`) is the direction-sensitive flag updater that writes the contact/block bits on the active object.
- `psx_object_register_contact_pair` (`0x8002845c`) is the bilateral contact queue helper used by both the broad sweep and the type-4 delayed path, which means the interaction lane is no longer speculative glue around the art/state system. It is a verified runtime bridge that can directly reseat the live script before the later `DAT_800758d4` companion lookup.
- The motion-heading side of that bridge is now closed one step further too:
- `psx_object_reselect_state_from_target_vector` (`0x80028c94`) is the older target-relative reselection helper used by `psx_type4_reselect_motion_state`. It builds a motion vector either from a target-relative offset or from a normalized target-to-player vector, writes that vector to `obj+0x60/+0x64/+0x68`, and reseats the live state script from the resulting heading bucket.
- `psx_object_quantize_motion_heading16` (`0x8003bc1c`) is the thin wrapper that feeds the current object motion vector into `psx_quantize_vector_heading16`.
- `psx_quantize_vector_heading16` (`0x8003b980`) is the actual 16-way heading quantizer. It classifies the current x/y vector against the threshold table at `DAT_80064990`, which is why the runtime reselection callers consistently reduce its result with `>> 2 & 0xf` before indexing the `DAT_800758cc` script table.
- The local post-advance render wrappers are also no longer anonymous labels:
- `psx_spawn_compound_record_advance_state_once` (`0x80013618`) creates one compound-record object, forces its script countdown to `1`, immediately runs `psx_object_advance_state_script`, and then marks the object with `obj+0x1e |= 0x20`. This is the cleanest currently recovered example of a constructor wrapper that intentionally advances into a non-initial live state before the object joins the normal update/render flow.
- `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) is the simpler sibling for the simple-record constructor: create the object, then immediately set the low active flag in `obj+0x1e`.
- `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is the compact stage-1 handoff wrapper. When the object still has a drawable resource and the `0x20` flag is set, it feeds the object through `psx_project_object_main_visible`; if the object is not in the `obj+0x1c & 1` hold state but does carry `obj+0x1e & 0x10`, it then runs the usual `FUN_80027f80` cleanup/follow-up path.
- `psx_object_advance_state_and_queue_special_visible` (`0x80013758`) is the compact stage-2 handoff wrapper. If the object still has a drawable resource, it advances the active script and immediately queues the object through `psx_project_object_special_visible_queue`, then applies a small sentinel cleanup block that clears world coordinates and selected flags for specific type/selector cases.
- The owner above those wrappers is now named too: `psx_object_integrate_motion_and_route_visible` (`0x800131a8`). It is the per-object bridge between movement state and rendering: it integrates position/velocity fields, refreshes the local visible/on-screen flags, handles the controlled-object side path, then advances the live state script and routes the object either into `psx_project_object_special_visible_queue` for the type-4/special-visible branch or into `psx_project_object_main_visible` for the normal drawable branch before the usual `FUN_80027f80` cleanup. This is the clearest recovered owner-level proof that state advancement and render-lane routing belong to the same runtime step.
- Those wrappers matter because they close one more gap between `psx_object_advance_state_script` and the render split. The stage-1/stage-2 divergence is not only visible in larger caller bodies such as the `0x80013524` / `0x80013564` branches; it also exists as small dedicated wrappers that either project through the main visible list after state work or advance-and-queue directly into the special-visible pass. That makes the renderer problem look even less like a missing flat table and more like a true runtime pipeline with multiple post-script routing paths.
- The render-side leaf chain is now close to end-to-end:
- `psx_project_object_main_visible` and `psx_project_object_special_visible_queue` both use the current script word at `obj+0x94` as the frame selector they pass into the frame-metric helpers.
- `psx_resource_frame_origin_x` (`0x8004513c`), `psx_resource_frame_origin_y` (`0x800451d0`), `psx_resource_frame_width` (`0x80045014`), and `psx_resource_frame_height` (`0x800450a8`) read per-frame rectangle metadata from the current drawable resource at `obj+0x10` using that same live frame index.
- `psx_draw_world_visible_passes` (`0x80041378`) is the top-level world-facing draw orchestrator: stage 1 draws the sorted main visible list through `psx_draw_main_visible_object` (`0x80041458`), stage 2 draws the queued special-visible list through `psx_draw_special_visible_queue` (`0x80041144`), and stage 3 is the non-world HUD/overlay pass.
- `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` then dispatch to one of two final frame submitters depending on resource kind: `psx_sprite_resource_submit_frame` (`0x80044bdc`) handles the streamed/VRAM-backed sprite resource path, while `psx_image_table_submit_frame` (`0x80044e9c`) handles the table-backed image resource path. Both receive the live frame index from `obj+0x94`, not the original authored selector.
- The stage-1 visible list manager is now named end to end too: `psx_main_visible_list_add`, `psx_main_visible_list_remove`, `psx_main_visible_list_rebucket_object`, `psx_main_visible_list_refresh_from_live_chain`, `psx_main_visible_list_sort_range`, and `psx_main_visible_list_get_sorted_slice` cover membership, rebucketing, refresh, dependency/tie-break sorting, and final slice retrieval for the main world-object draw pass.
- The constructor/resource side closes the remaining resource-pointer handoff. `psx_create_image_resource_from_descriptor` is now confirmed as the builder used by both constructors: type-4 descriptors bind a single indexed image resource through `image_resource_bind_vram_slot`, while type-5 descriptors allocate and upload a multi-frame bundle through `image_bundle_load_to_vram`. Both `psx_object_create_simple_record` and `psx_object_create_compound_record` resolve the per-type art bank before any draw pass runs, store the resulting drawable resource on the object at `obj+0x10`, store the per-type variant bank at `obj+0x84`, store the per-type state-script bank at `obj+0x88`, and only then call `psx_object_select_state_script`. So the current strongest end-to-end model is now:
- authored record `type` -> per-type art/state/variant banks
- constructor seeds `obj+0x10` drawable resource plus `obj+0x88/0x84`
- `psx_object_select_state_script` seeds the initial script from `DAT_800758cc`
- runtime reselection / `psx_object_advance_state_script` updates the live script word at `obj+0x94`
- `psx_object_lookup_variant_entry` maps that live script word through `DAT_800758d4`
- the projectors and final draw submitters use the same live `obj+0x94` frame index against the drawable resource at `obj+0x10`
- The newly traced consumer side changes the export diagnosis in an important way. `psx_object_advance_state_script` does not treat the `DAT_800758d4` lookup result as a resource id or replacement frame index; it sign-extends the returned three bytes into `obj+0x30/+0x34/+0x38`, and the verified downstream consumers are all in the overlap/contact lane. `psx_object_test_overlap_3d` uses those fields as box extents against `obj+0x54/+0x58/+0x5c`, `psx_object_update_contact_block_flags` uses the same extents while updating directional block bits, and the reselection helpers read target-object `+0x30/+0x34/+0x38` as target bounds. By contrast, the visible projectors and both stage-1/stage-2 draw helpers still only use `obj+0x10` plus live `obj+0x94` for visible presentation.
- The renderer/exporter path now preserves that distinction too. The PSX cache builder decodes `DAT_800758d4` as a `count + packed signed xyz-extents` table, carries the decoded per-state tuples in the exported state layer, and writes the resolved `companionExtents` tuple onto each scene item and `mapSource` row for the chosen live state when a matching entry exists. So the viewer now keeps the newly verified bounds evidence explicitly instead of flattening it back into the old placeholder-art bucket.
- That is not yet the full remaining answer for unresolved placeholder-heavy families, but the missing piece is narrower again now. The `DAT_800758d4` bytes are no longer the best candidate for the final art table; they are acting like per-state companion extents used by collision/contact logic. So the remaining gap sits inside the family-specific live resource/frame presentation path after script reselection, not in projection, placement decoding, draw-lane discovery, or generic `DAT_800758d4` consumption.
- The latest dispatch-table trace also removes one remaining false lead for `0x0042` and `0x0049`. Their type-table slots do not point to unique family handlers; both currently resolve to the same generic descriptor used across `0x003e..0x0050`, whose callback slots are `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and `psx_object_release_to_free_list`. So the unresolved art rule for those types is not hiding in a dedicated per-type descriptor fork; it still appears to live later in generic object state/resource handling.
- The state/art split is therefore even sharper than the earlier constructor notes implied. `psx_object_select_state_script` only chooses the current script base from `DAT_800758cc` and stores the authored selector at `obj+0x9e`, but both `psx_object_reselect_state_from_target_vector` and `psx_type4_reselect_motion_state` can later overwrite that live script choice from runtime motion state. `psx_object_advance_state_script` then refreshes `obj+0x94` from the current script word and reruns `psx_object_lookup_variant_entry`, and that lookup indexes `DAT_800758d4` by `obj+0x94`, not by the original `obj+0x9e` selector byte.
- That changes the exporter diagnosis in a useful way. The unreadable map output is still real, but the remaining gap is narrower than before: it is not just that `DAT_800758cc` exists, but that runtime interaction and heading-state paths can overwrite the live script after spawn. Any renderer-safe state-to-art rule for unresolved families such as `0x0042` has to account for those post-spawn reselection paths instead of assuming the authored selector byte is final.
- that means the PSX level load now has four distinct evidence-backed layers instead of the earlier two-way split:
- root dispatch records at `DAT_800678f4`
- secondary `0x18`-stride records at `DAT_80067720`
- per-type runtime banks at `DAT_800758d8/d0/cc/d4`
- a dedicated `0x50` level runtime-header block at `DAT_80067794`
- plus the separately loaded compressed source at `DAT_8006767c` and its optional `DAT_8006b5d8 -> DAT_8006769c` decoded `0x3e00` state buffer
## PSX Coordinate Model From Executable
The current coordinate problem is no longer a renderer-only guess. The executable now closes the last projection step well enough to treat PSX placement as its own map-space model instead of as a PC-style direct world export.
@ -800,7 +904,7 @@ Open parts that still matter:
- this closes the final world-to-screen math, but it does not yet prove which raw `post_audio_region_01` or `post_audio_region_00` record family feeds each constructor path
- it also does not close the type/resource lookup that selects the correct bundle/frame through `DAT_800758cc/d0/d4/d8`
- palette override remains a separate unresolved control path layered on top of the now-understood projection math
- palette override is no longer wholly unresolved in the viewer path, but the `>= 0x00ac` source-base semantics and the runtime's later variant reselection still leave some color choices provisional
Immediate consequence for the next pass: