Crusader_Decomp/docs/psx/map-rendering.md

1833 lines
111 KiB
Markdown
Raw Normal View History

2026-04-07 17:16:44 +02:00
# PSX Map Rendering Architecture
## Scope
This document explains the current evidence-backed model for how the PlayStation build stores map data, how the executable turns that data into live objects and visible primitives, and how a viewer can reassemble the same data into a coherent scene.
It is not a replacement for the running note in `docs/psx/psx.md`. Instead, it consolidates the map-rendering findings from that note and the related Ghidra work into one detailed technical reference.
Primary target:
- retail PlayStation `SLUS_002.68`
Primary use:
- guide viewer/exporter work in `Crusader-Map-Viewer`
- preserve the executable-backed reasoning behind the current PSX scene format
- make the remaining unresolved gap explicit instead of leaving it spread across many small notes
## Executive Summary
The current strongest model is:
1. A PSX level is not stored as one flat placement table.
2. `LSET*.WDL` loads a multi-section bundle with at least three map-relevant classes of data:
- authored root/dispatch records
- authored constructor-placement records
- per-type runtime banks for art, state scripts, variants, and simple-record payloads
3. Constructors turn those authored rows into live objects with fixed-point world coordinates, a cached drawable resource pointer, a state-script bank pointer, a variant bank pointer, and a preserved pointer back to the original authored record.
4. The runtime does not draw directly from the authored selector byte. It advances and sometimes reseats the live state script after spawn, then uses the current script word to drive frame choice and variant lookup.
5. There are two separate world-facing render lanes:
- stage 1: main visible-object list
- stage 2: queued special-visible list
6. The viewer can already reconstruct placement, projection, most resource loading, and much of the draw path from executable evidence.
7. The main remaining blocker is the last live state-to-art rule for unresolved families such as `0x0042` and `0x0049`. The map is still unreadable in practical terms because those families still fall back to placeholders.
2026-04-12 14:45:08 +02:00
8. The current `map 104` repeated-wall regression is now clearly over-merged. The cache shows `type=0x0042` records from both authored section-0 families and multiple raw `u5` lane/class values still collapsing onto the same donor wall bundle, which is stronger evidence of a wrong runtime family/resource merge than of a missed palette variant.
9. The latest six-track Ghidra pass closes one more structural question: `0x0042` is not special at descriptor time. It shares the same generic descriptor cluster as the wider `0x003e..0x0050` band, so the practical split the viewer still needs is downstream in state, flags, lane routing, and resource kind.
10. A focused live cleanup pass on the update and ordering lane now closes the missing-function/object gap around `0x80012b44`: the parent routine is now explicitly modeled as `psx_object_integrate_motion_and_route_visible` (`0x8001263c..0x80012c2c`), with the local control helper named `psx_object_update_runtime_input_modes` (`0x80012c30`). The stage-1 ordering lane also now has explicit helper names/contracts for `psx_main_visible_list_swap_entries` (`0x8002e064`), `psx_main_visible_order_graph_unlink_pair` (`0x8002ca74`), and `psx_main_visible_order_graph_detach_object` (`0x8002c89c`), reducing ambiguity in rebucket/sort refresh behavior used by the exporter.
11. The next concrete `map 104` `0x0042` pass now ties the exporter-side `runtimeDiagnostic` schema back to named live code instead of only to theory. The root and constructor section-0 families now have explicit named entry points but still converge through the same shared `0x0042` descriptor row, constructors are now proven to seed `obj+0x1c` by directly copying the authored lane word, the strongest recovered `0x0400` stage-selection write is still nested-state-side rather than a direct object-local `0x0042` writer, and `DAT_800675f8` is now tighter as a level-loaded per-type policy pointer instead of a per-lane discriminator.
12. A focused object-local route-bit provenance pass over the fixed `map 104` `0x0042` sample pack (`item:25/30/31/35/85/86`) now tightens the `obj+0x1c & 0x0400` branch model: `psx_object_create_simple_record` (`0x80024b48`) and `psx_object_create_compound_record` (`0x80025040`) remain the strongest concrete object-local writers because they copy authored `u5` directly into `obj+0x1c`; downstream named mutators (`psx_type42_transition_selector_tick`, `psx_object_select_state_from_transition_table`, `psx_object_advance_state_script`, `psx_apply_deferred_control_to_live_objects`, `psx_object_handle_control_pair_0a`) mutate other bits but do not introduce an object-local `0x0400` set/clear path; and recovered `0x0400` writers in this lane are still nested/global (`psx_object_state_machine_dispatch_tick` nested runtime word at `0x8001a078`, global policy word in `psx_object_handle_control_pair_0a` at `0x80022a14`).
13. A focused visibility-routing/final-draw pass now closes the strongest remaining ambiguity between stage lanes and submitter rules. `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) now has an explicit stage split note (`type==4 || flags_1c&0x0400` routes to stage-2 special queue, else stage-1 main-visible), stage-1 order graph helpers are named/commented (`0x8002c89c`, `0x8002ca74`, `0x8002d778`, `0x8002e064`), draw pass ordering is pinned (`0x80041378`), submitter dispatch is pinned (`kind==5` image-table else sprite), and CLUT selection is now named through `psx_clut_table_by_resource_bank` (`0x800a9f48`) plus `psx_clut_override_table_by_palette_token` (`0x800a9f66`).
2026-04-07 17:16:44 +02:00
So the problem is no longer "how do PSX coordinates work" or "where do draw rectangles come from". The problem is now much narrower: the viewer still does not fully reproduce the executable's final runtime art-state resolution.
2026-04-12 14:45:08 +02:00
## Final Live Map-104 Cohort Pass (2026-04-12)
Pass objective:
- close the final cohort split question for scene fingerprint `3497e7f641856415` on active writable `SLUS_002.68`
- keep scope fixed to anchor groups already sampled in cache diagnostics:
- root `0x0022`: items `25/35`
- root `0x0030`: items `30/31`
- constructor `0x0030`: items `85/86`
- control `0x0066`: item `53`
Functions inspected in this pass (create/update/draw/control path):
- `psx_object_create_simple_record` (`0x800249f4`) - edited (comment)
- `psx_object_create_compound_record` (`0x80024eec`) - edited (comment)
- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) - edited (comment)
- `psx_object_advance_state_script` (`0x80025d68`) - edited (comment)
- `psx_draw_main_visible_object` (`0x80041458`) - edited (comment)
- `psx_draw_special_visible_queue` (`0x80041144`) - inspected (no new edit)
- `psx_main_visible_order_compare_pair_for_graph` (`0x8002be6c`) - inspected (no new edit)
- control-family callbacks (`0x80013618`, `0x80013688`) - inspected (no new edit)
Live artifacts changed in this pass:
- decompiler comments added/updated at:
- `0x800249f4`
- `0x80024eec`
- `0x800131a8`
- `0x80025d68`
- `0x80041458`
Concrete cohort conclusions (what still differs vs still fails to differ):
1. Still differs (strong): authored route seed in section-0 records remains the cleanest stable split for this sample set. Constructor and root create paths both copy authored route word directly into `obj+0x1c`, so root `0x0022` and `0x0030` are preserved at spawn instead of being synthesized later.
2. Still fails to differ (strong): root `0x0030` (`30/31`) and constructor `0x0030` (`85/86`) are route-equivalent at creation (`obj+0x1c` seed), so this pair should not be split by constructor-vs-root family alone.
3. Still fails to differ (current capture): stage split bit `obj+0x1c & 0x0400` remains clear in sampled anchors, so all cohorts continue through stage-1 main-visible rather than diverging by stage lane.
4. Still fails to differ (current capture): no recovered per-cohort difference in submitter class at draw callsites yet; submitter remains bound-resource-kind based (`kind==5` image-table else sprite) and current anchors lack a proven mixed-kind split.
5. Still differs (secondary but real): main-visible draw injects authored palette-token high byte while special-visible does not. This remains a lane behavior split, but because sampled anchors currently route main-visible, it is not yet the primary divider among these specific cohorts.
Strongest evidence for the next exporter rule:
- Use authored route seed (`u5` -> `obj+0x1c`) as the first unresolved-family splitter for map-104 `0x0042` placeholders, with `0x0022` and `0x0030` kept in separate fallback buckets until live capture proves convergence on resource kind plus latched frame token.
Safe immediate renderer/exporter change suggested by this pass:
- keep same-type unresolved placeholders separated by authored route seed for this fingerprint:
- bucket A: `0x0042` + route seed `0x0022`
- bucket B: `0x0042` + route seed `0x0030`
- do not split `0x0030` bucket by root vs constructor origin alone.
- do not promote `bit0x0400` or policy-word heuristics to primary keys for these anchors until runtime diagnostics actually sample non-null values.
## Latest Loader/Install Pre-Constructor Pass (2026-04-12)
Pass objective:
- close the pre-constructor loader/install side for graphics-critical state on active writable `SLUS_002.68`
- recover concrete semantics for WDL bundle load, CLUT install, detached runtime-stream install, and per-type bank population
- apply only conservative live Ghidra edits where evidence is direct from decompile/disassembly
Functions inspected (loader/install focus):
- `wdl_resource_bundle_load_by_index` (`0x80039444`) - edited (comment)
- `psx_install_type_state_script_component_extents_banks` (`0x8003917c`) - edited (comment)
- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`) - edited (comment)
- `level_palette_header_apply` (`0x8002badc`) - edited (comment)
- `FUN_80040768` -> `psx_install_level_audio_runtime_stream_bundle` (`0x80040768`) - edited (rename + comment)
Live artifacts changed in this pass:
- rename:
- `0x80040768`: `FUN_80040768` -> `psx_install_level_audio_runtime_stream_bundle`
- decompiler comments added/updated at:
- `0x80039444`
- `0x8003917c`
- `0x80045ffc`
- `0x8002badc`
- `0x80040768`
Recovered semantics (bundle/CLUT/runtime-bank install):
1. `wdl_resource_bundle_load_by_index` performs a staged multi-section install before constructor dispatch: it reads per-level section sizes, installs type art/state lanes from two bundle passes, installs section-pack pointers (`policy`, `control opcode stream`, `CLUT table`), applies palette/CLUT header, installs detached runtime stream payload, optionally inflates persistent runtime-state blob, and only then dispatches root records.
2. CLUT install is explicit in `level_palette_header_apply`: packed palette header fields are decoded, palette data is expanded, and CLUT blocks are uploaded through `level_palette_upload_cluts` while level CLUT mode/state globals are updated.
3. Per-type state/runtime bank install is explicit in `psx_install_type_state_script_component_extents_banks`: it writes `psx_type_state_script_bank`, `psx_type_simple_component_bank`, and `psx_type_companion_extents_bank` by type row before constructors consume those lanes.
4. Per-type art install is explicit in `psx_install_type_art_active_header_and_built_resource`: type art header slot is written, kind-4/5 resource is resolved/built, built-resource cache is committed, and the active slot is mirrored to resolved runtime resource for constructor-side binding.
5. Detached runtime stream install is explicit in `psx_install_level_audio_runtime_stream_bundle`: it initializes SPU/sequence runtime from the detached blob header, maps 9 stream chunks, opens sequence/VAB handles, uploads stream payload to SPU RAM, and commits voice/channel defaults before object constructors run.
Strongest evidence for what is available before constructors run:
- per-type art lane is already installed (`psx_type_art_active_header_bank` / `psx_type_art_built_resource_bank`)
- per-type behavior/state lanes are installed (`psx_type_state_script_bank`, `psx_type_simple_component_bank`, `psx_type_companion_extents_bank`)
- type-policy pointer table and control-opcode stream table are installed from section-pack offsets
- level CLUT table and expanded/uploaded CLUT data are installed via palette header apply path
- detached runtime stream payload (audio sequence/bank runtime) is installed and SPU runtime is initialized
Concrete exporter implication (avoid placeholder families):
- export should model loader output as the authoritative pre-constructor state boundary, not only section-0 authored rows.
- for unresolved visible families, placeholder fallback should be delayed until after reconstructing this install chain:
- type art active/built lanes
- type state/component/extents lanes
- type policy lane
- CLUT table + palette override readiness
- practical consequence: resource-kind or frame-state donor heuristics that ignore preinstalled type/art/CLUT lanes will continue to collapse distinct runtime families into repeated placeholder walls.
## Latest Draw Submission, Resource-Kind Dispatch, And Palette Token Pass (2026-04-12)
Pass objective:
- close the visible draw submission side with direct evidence from the two world-visible lanes
- pin exactly how bound resource kind and frame token select submitter and CLUT path
- document exporter-facing implications for unresolved placeholder families
Functions inspected in this pass:
- `psx_draw_world_visible_passes` (`0x8004137c`)
- `psx_draw_main_visible_object` (`0x80041458`)
- `psx_draw_special_visible_queue` (`0x80041144`)
- `psx_project_object_main_visible` (`0x80040d44`)
- `psx_sprite_resource_submit_frame` (`0x80044bdc`)
- `psx_image_table_submit_frame` (`0x80044e9c`)
Live artifacts changed in this pass:
- decompiler comments added/updated at:
- `0x8004137c`
- `0x80041458`
- `0x80041144`
- `0x80040d44`
- `0x80044bdc`
- `0x80044e9c`
Recovered semantics (draw submission and palette token):
1. World draw order is fixed by `psx_draw_world_visible_passes`: stage-1 sorted main-visible draw first, then stage-2 special-visible queue, then HUD/overlay.
2. Both visible world lanes use the same resource-kind dispatch at draw call sites:
- if `**(obj+0x10) == 5` -> `psx_image_table_submit_frame`
- else -> `psx_sprite_resource_submit_frame`
3. Main-visible injects authored palette token into submit flags before dispatch:
- base flags include `obj_flags & 0x0002`
- for `type 0x003e..0x00ab`: token high byte from `source+0x06`
- for `type >= 0x00ac`: token high byte from `source+0x0c`
4. Special-visible does not inject authored palette token high byte; it passes only orientation/flip bits (`obj_flags & 0x0002`) into submitters.
5. Submitters converge on the same CLUT rule:
- default: `psx_clut_table_by_resource_bank[resource_bank]`
- override when high-byte token present: `psx_clut_override_table_by_palette_token[(submit_flags >> 8)]`
6. Frame token bridge is explicit in projection and submit:
- `obj+0x94` is used by `psx_project_object_main_visible` frame-origin/size helpers
- same token is passed to sprite/image-table submitters as the per-object visible frame selector
Strongest main-visible vs special-visible evidence split:
- Main-visible (`0x80041458`) computes and ORs a high-byte palette token before submit.
- Special-visible (`0x80041144`) performs the same kind dispatch but omits token injection.
- Because submitters only apply CLUT override when nonzero high-byte token exists, this omission is the strongest executable-backed reason the two lanes can render with different palette behavior even for comparable resource kinds.
Concrete exporter implication for remaining placeholder families:
- Keep world visible lanes distinct in export/replay and diagnostics:
- lane `main-visible`: allow authored palette-token override into CLUT selection.
- lane `special-visible`: do not apply authored token override unless a separate stage-2 token source is recovered.
- For unresolved families (`0x0042`, `0x0049`, `0x0055..0x0063`), include both fields in runtime diagnostics and donor-key matching:
- bound resource kind (`sprite` vs `image-table` path)
- latched frame token (`obj+0x94`)
- Avoid cross-kind donor fallback (sprite <-> image-table) because submitter path and frame metadata semantics differ at callsite level.
## Latest Selector/Transition Pre-Latch Pass (2026-04-12)
Pass objective:
- close pre-latch selector and transition-row semantics for unresolved visible families, centered on type `0x0042`
- separate early gating and selector reseat (`obj+0x9e` path) from final frame latch (`obj+0x94` path)
Functions inspected in this pass:
- `psx_type42_transition_selector_tick` (`0x80018578`)
- `psx_object_select_state_from_transition_table` (`0x8001bca0`)
- `psx_object_is_within_view_margin` (`0x8001e6e8`)
- `psx_object_select_state_script` (`0x800260e8`)
- `psx_object_advance_state_script` (`0x80025d68`)
Live artifacts changed in this pass:
- decompiler comments added/updated at:
- `0x80018578`
- `0x8001bca0`
- `0x8001e6e8`
- `0x800260e8`
- `0x80025d68`
Recovered selector/transition semantics (exact current read):
1. Type-`0x0042` pre-latch reseat is explicitly gated before selector edits: `psx_type42_transition_selector_tick` first requires `psx_object_is_within_view_margin(obj, 0x14)` and object lane bit `obj+0x1c & 0x0020` before its turn/heading reseat path proceeds.
2. Transition row selection is table-indexed and type-relative in `psx_object_select_state_from_transition_table`: `transition_code = DAT_80063a00[(type-0x1e)*0x0f + slot]`, then selector base from `DAT_80063b4c[(type-0x1e)*0x0f + transition_code]`.
3. For type `0x0042`, the recovered rows are now byte-concrete from live memory:
- mode/policy row @ `0x80063c1c`: `2d 00 00 05 0a 14 0f 19 23 23 28 00 00 00 1e`
- selector row @ `0x80063d68`: `3c 00 00 00 50 0f 00 00 00 19 00 00 32 32 00`
4. Selector `3/4` (and neighboring headings) feed art choice through the pre-latch path, not by directly writing the final frame token: turn/heading branches in `0x80018578` call `psx_object_select_state_script` with wrapped heading buckets (`& 7`) before latch.
5. `psx_object_select_state_script` is confirmed as selector install only (`obj+0x9e`, `obj+0x8c/0x90` cursor); final visible token still latches later in `psx_object_advance_state_script` via write to `obj+0x94`.
Strongest evidence for selector-to-visible-art linkage:
- The same live chain now stays consistent across all inspected callsites: pre-latch selector reseat (`0x80018578` and `0x8001bca0`) -> selector install (`0x800260e8`) -> frame/state latch (`0x80025d68`, write to `obj+0x94`) -> projection/draw frame queries. This is the clearest current evidence that selector `3/4` effects are real but indirect: they bias the later latched frame token rather than bypassing it.
Exporter implication for map `104` and remaining placeholder families:
- Treat selector and latch as separate channels in export logic for unresolved families (`0x0042`, `0x0049`, `0x0055..0x0063`):
- `preLatchSelector`: from reseat/install lane (`obj+0x9e` path)
- `latchedFrameToken`: final draw-driving token (`obj+0x94`)
- Do not collapse unresolved cases into one donor wall when pre-latch selectors differ but latches are unknown. For map `104`, this pass further supports splitting placeholder cohorts by selector/transition row behavior before any cross-family donor fallback.
## Latest Type-Art Install And Constructor-Bind Pass (2026-04-12)
Pass objective:
- tighten the active-header/built-resource install lane around `0x800758d8` and `0x800758c8`
- lock a conservative, evidence-backed install->constructor->draw chain for unresolved map-viewer families
Functions inspected (focused set):
- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`)
- `psx_object_create_simple_record` (`0x800249f4`)
- `psx_object_create_compound_record` (`0x80024eec`)
- `psx_stream_install_type_runtime_banks` (`0x80038f18`)
- `psx_create_image_resource_from_descriptor` (`0x80044434`)
- `psx_draw_main_visible_object` (`0x80041458`)
Live artifacts changed in this pass:
- decompiler comments added/updated at:
- `0x80045ffc`
- `0x800249f4`
- `0x80024eec`
- `0x80038f18`
- `0x80041458`
Strongest install->draw evidence recovered:
1. `psx_install_type_art_active_header_and_built_resource` first writes the incoming header pointer into `psx_type_art_active_header_bank[type]` (`0x800758d8` lane), then performs kind dispatch (`4` single-image bind, `5` bundle build/upload) and commits the resolved runtime resource pointer into `psx_type_art_built_resource_bank[type]` (`0x800758c8` lane).
2. The same installer mirrors the active slot to the resolved runtime resource pointer after build/reuse, so constructor-time reads observe resolved per-type runtime resource state, not only raw descriptor headers.
3. Both constructors (`simple` and `compound`) bind `obj+0x10` from the per-type lane: if active entry kind is `5`, they reuse `psx_type_art_built_resource_bank[type]`; otherwise they invoke `psx_create_image_resource_from_descriptor` and refresh cache state.
4. `psx_stream_install_type_runtime_banks` seeds/clears type runtime bank slots and resets built-resource cache entries during stream install, preserving a clear ownership boundary between stream payload install and later resource realization.
5. `psx_draw_main_visible_object` uses ctor-bound `obj+0x10` plus live frame token `obj+0x94`; submitter choice is resource-kind based (`kind==5` -> `psx_image_table_submit_frame`, else `psx_sprite_resource_submit_frame`).
Conservative semantics confirmed in this pass:
- `0x800758d8` is the active per-type art lane at install/constructor handoff time.
- `0x800758c8` is the built-resource cache lane used for kind-5 reuse and refreshed through install/build paths.
- Constructor binding and draw submission are directly connected through `obj+0x10` (resource) and `obj+0x94` (live frame token), so unresolved visible families should be debugged as runtime lane/state/resource issues rather than as missing top-level section-0 decode.
Exporter-facing implication for unresolved map-104 families:
- For unresolved `0x0042` bands, do not treat type-level art as a static `type -> bundle` table. The executable path is `type install` -> `active/built per-type lane` -> `ctor object bind` -> `live frame token` -> `kind-based submitter`.
- Mixed-family donor heuristics should remain fenced unless they match this chain (especially resource kind parity and frame-state behavior), because map-104 failures are now better explained by wrong runtime resource-family merges than by missing section-0 placement decoding.
## Latest Visibility Routing, Ordering, And Draw-Lane Pass (2026-04-12)
Pass objective:
- close final routing/order/submitter ambiguity around stage-1 versus stage-2 world draw lanes
- keep edits conservative and evidence-backed in live `SLUS_002.68`
Functions and helpers inspected in this pass:
- routing/input lane:
- `psx_player_object_integrate_motion_and_route_visible` (`0x8001263c`, renamed this pass from duplicate generic name)
- `psx_object_update_runtime_input_modes` (`0x80012c30`)
- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`)
- stage-1 order graph lane:
- `psx_main_visible_order_graph_detach_object` (`0x8002c89c`)
- `psx_main_visible_order_graph_unlink_pair` (`0x8002ca74`)
- `psx_main_visible_list_sort_range` (`0x8002d778`)
- `psx_main_visible_list_swap_entries` (`0x8002e064`)
- draw and submit lane:
- `psx_draw_world_visible_passes` (`0x80041378`)
- `psx_draw_special_visible_queue` (`0x80041144`)
- `psx_draw_main_visible_object` (`0x80041458`)
- `psx_sprite_resource_submit_frame` (`0x80044bdc`)
- `psx_image_table_submit_frame` (`0x80044e9c`)
Live edits applied in this pass:
- rename:
- `0x8001263c`: `psx_object_integrate_motion_and_route_visible` -> `psx_player_object_integrate_motion_and_route_visible`
- data labels:
- `0x800a9f48` -> `psx_clut_table_by_resource_bank`
- `0x800a9f66` -> `psx_clut_override_table_by_palette_token`
- decompiler comments:
- `0x800131a8`: explicit stage split (`type==4 || flags_1c&0x0400` -> stage-2 special queue)
- `0x8002d778`: stage-1 dependency sorter and policy-bit influence (`0x0008`, `0x0600`)
- `0x8002ca74`: directed order-edge unlink contract
- `0x8002c89c`: full order-graph detach contract
- `0x80041378`: fixed world draw pass sequence (stage-1, stage-2, then HUD)
- `0x80041458`: main-visible submitter and palette-token override rule
- `0x80041144`: stage-2 submitter rule parity, no authored palette-token override
- `0x80044bdc`: sprite submit upload/refresh and CLUT override behavior
- `0x80044e9c`: image-table submit metadata-only frame resolve and CLUT override behavior
Strongest evidence recovered:
1. Stage routing split in `0x800131a8` is explicit and branch-local: `type==4 || (obj+0x1c & 0x0400)` calls `psx_project_object_special_visible_queue`; the fallthrough path calls `psx_project_object_main_visible`.
2. World pass submission order in `0x80041378` is explicit and fixed: iterate sorted main-visible slice -> draw special-visible queue -> draw HUD/overlay.
3. Stage-1 ordering is not a plain z-sort: `0x8002d778` repeatedly resolves dependency counts and may call `psx_main_visible_order_graph_unlink_pair`; graph maintenance is handled by `0x8002ca74` and bulk detach by `0x8002c89c`.
4. Submitter choice is lane-independent and strictly resource-kind based at both call sites (`0x80041458`, `0x80041144`): `kind==5` routes to `psx_image_table_submit_frame`; else `psx_sprite_resource_submit_frame`.
5. Palette handling differs by lane: main-visible computes an authored high-byte token from source fields (`+0x06` for `0x003e..0x00ab`, `+0x0c` for `>=0x00ac`) and ORs it into submit flags, while special-visible currently passes only orientation-flip bit (no authored token injection).
6. Both submitters converge on identical CLUT override semantics: default from `psx_clut_table_by_resource_bank`, optional override through `psx_clut_override_table_by_palette_token[(flags>>8)]` when high-byte token is present.
Renderer implication for map-viewer quality:
- stage-1/stage-2 should remain distinct queues through export and replay; merging them collapses ordering and palette behavior.
- ordering must preserve graph constraints, not only depth heuristics.
- frame submitter must be selected from bound resource kind (`5` image-table, otherwise sprite) independent of lane.
- palette override behavior should be lane-aware: apply authored token override in main-visible only; keep stage-2 on baseline CLUT unless separate evidence introduces a stage-2 override source.
## 2026-04-12 Exporter Follow-Through
The renderer/cache builder now follows two of the strongest executable-backed constraints from the visibility/draw pass:
1. Authored palette override bytes are only consumed for records whose authored route flags currently classify them as main-visible. If a record carries a palette token but routes to the special-visible lane, scene export now preserves that token only as diagnostics and keeps the rendered palette on the bundle-default/heuristic path.
2. Cross-map donor reuse is now fenced by recovered active-header `payloadKind` from `DAT_800758d8`, so the exporter no longer treats image-table-backed and sprite-backed types as interchangeable donor candidates when borrowing unresolved art.
That does not close the remaining unresolved families by itself, but it removes two broad sources of false positives:
- stage-2 objects inheriting authored palette overrides that the executable does not inject
- unresolved types borrowing donors from the wrong resource-kind family
## Latest Runtime/Control-Island Policy Pass (2026-04-12)
Pass objective:
- test whether the runtime/control island around `0x80063e54` / `0x80063e68` / `0x800675ec` and per-type policy table `0x800675f8` can directly explain unresolved visible-art family splits on `map 104`
- apply only conservative live edits for helpers where behavior is directly evidenced
Functions inspected in this pass (focus set):
- `psx_level_post_load_runtime_reset` (`0x80039ef4`) - edited (comment)
- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`, policy read at `0x8001353c`) - edited (comment)
- `psx_draw_main_visible_object` (`0x80041458`, policy read at `0x80041604`) - edited (comment)
- `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`) - edited (comment)
- passcode/control state block around `0x80034d60` (table-pair control gate) - edited (disassembly comment)
Conservative helper renames landed (runtime-marker island family):
- `0x8002e598`: `FUN_8002e598` -> `psx_marker_channel_runtime_state_init_heap_block`
- `0x8002e484`: `FUN_8002e484` -> `psx_marker_channel_get_mode2_meter_value`
- `0x8002e498`: `FUN_8002e498` -> `psx_marker_channel_try_spend_mode2_meter`
- `0x8002e3e8`: `FUN_8002e3e8` -> `psx_marker_channel_add_mode2_meter_and_queue_event`
- `0x800308ac`: `FUN_800308ac` -> `psx_marker_channel_refresh_mode2_active_slot`
- `0x800304c4`: `FUN_800304c4` -> `psx_marker_channel_cycle_active_slot_by_direction`
Recovered island/policy interaction semantics (strongest current read):
1. `0x80063e68` and `0x80063e54` act as a reciprocal control-gating pair in post-load/passcode transition flow: selected map id maps to slot via `0x80063e68`, then is validated by reverse map lookup via `0x80063e54` before mode-action side effects proceed.
2. `0x800675ec` is the marker-channel runtime-state block pointer used by loadout/mode-action/reset and marker event helpers; this is a control/runtime lane, not a direct per-object art-resource lane.
3. `0x800675f8` policy words are consumed in routing/order/draw as behavior bits by type:
- route-side read (`0x80013550`) uses `0x1000` for nearby-interaction publication behavior after lane route decision
- draw-side read (`0x8004161c`) uses `0x2000` for a render-state branch after submitter/lane are already chosen
- order-graph compare (`0x8002bf2c`) uses `0x0600` class behavior during pair ordering
Strongest evidence for/against this being the visible-art family split:
- Against direct lane-split causality (strong): stage-1 vs stage-2 world-visible lane choice is object-local `obj+0x1c & 0x0400` at `0x80013518`, not a direct read of `0x800675f8` or the `0x63e54/0x63e68/0x675ec` island.
- Against direct submitter-split causality (strong): sprite vs image-table submitter selection is resource-kind based at submit sites, before/independent of policy bit tests.
- For secondary visual influence (moderate): policy bits (`0x2000`, `0x0600`, `0x1000`) can still change render-state branch, draw ordering class, and interaction publication. These can alter final presentation behavior but are not currently the primary discriminator for unresolved family resource/lane identity.
Concrete exporter implication for unresolved `map 104` cohorts:
- keep island/policy diagnostics, but do not use them as first-key art-family split selectors:
- primary split keys remain bound resource kind, `obj+0x94` latched frame token, and route bit `obj+0x1c & 0x0400`
- island/policy words should be recorded as secondary modifiers (`orderingClass`, `drawPolicyBits`, `publishPolicyBits`) to explain same-resource visual divergence
- practical fallback rule: if cohorts differ only by island/policy bits and not by resource kind/frame token/route lane, treat them as one art family with policy-variant rendering, not separate base-art families.
## Latest Storage-Mapping Pass (2026-04-11)
Pass objective:
- stay on live PSX storage ownership and runtime-bank install chain for unresolved `map 104` type `0x0042`
- avoid draw-path broadening and focus on subordinate table/blob ownership
Functions and globals inspected in this pass:
- loader and blob chain:
- `wdl_resource_bundle_load_by_index` (`0x80039444`)
- `psx_lzss_unpack_into_level_buffer` (`0x8003b00c`, renamed this pass)
- `psx_lzss_pack_level_buffer` (`0x8003aba8`, renamed this pass)
- `psx_load_type_state_banks` (`0x8003917c`)
2026-04-13 15:59:50 +02:00
- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`)
2026-04-12 14:45:08 +02:00
- section-0 authored dispatch:
- `psx_dispatch_section0_dispatch_roots` (`0x800256b0`)
- `psx_dispatch_section0_constructor_placements` (`0x800258cc`)
- type-`0x0042` descriptor row pointer/value anchors:
- `0x80063220` -> `0x800626f8`
- row callbacks include `psx_spawn_compound_record_advance_state_once` (`0x80013618`), `psx_spawn_simple_record_set_active_flag` (`0x8001372c`), and update/release slots
- constructor/runtime-bank consumers:
- `psx_object_create_simple_record` (`0x800249f4`)
- `psx_object_create_compound_record` (`0x80024eec`)
- `psx_draw_main_visible_object` (`0x80041458`)
- `psx_family_wrapper_spawn_compound_pair_y_and_type42_mode_gate` (`0x800230e4`)
- key storage globals:
- `psx_level_decompressed_state_buffer` (`0x8006769c`)
- `psx_level_state_compressed_blob` (`0x8006b5d8`, renamed this pass)
- `psx_level_heap_cursor` (`0x8006763c`, renamed this pass)
- `psx_section0_dispatch_root_records` (`0x80067720`)
- `psx_section0_constructor_placement_records` (`0x800678f0`)
- `DAT_80067938`, `DAT_80067838`, `DAT_80067840`, `DAT_800676d8`
- `psx_type_policy_table_ptr` (`0x800675f8`)
- `psx_type_art_active_header_bank` (`0x800758d8`)
- `psx_type_art_built_resource_bank` (`0x800758c8`)
- `psx_type_state_script_bank` (`0x800758cc`)
- `psx_type_simple_component_bank` (`0x800758d0`)
- `psx_type_companion_extents_bank` (`0x800758d4`)
Applied live renames/comments in this pass:
- renames:
- `FUN_8003b00c` -> `psx_lzss_unpack_into_level_buffer`
- `FUN_8003aba8` -> `psx_lzss_pack_level_buffer`
- `DAT_8006b5d8` -> `psx_level_state_compressed_blob`
- `DAT_8006763c` -> `psx_level_heap_cursor`
- comments:
- at `0x80039af0`: compressed level-state blob loads into `psx_level_state_compressed_blob`, then unpacks into `psx_level_decompressed_state_buffer` before root/constructor dispatch
- at `0x800249f4`: constructor bind note for `psx_type_art_active_header_bank[type]` and `psx_type_art_built_resource_bank[type]`
Recovered ownership chain (strongest current read):
1. `wdl_resource_bundle_load_by_index` reads the level section header, allocates contiguous level heap storage, and installs section pointers (`psx_section0_dispatch_root_records`, `psx_section0_constructor_placement_records`, plus subordinate slices such as `DAT_80067938`, `DAT_80067838`, `psx_type_policy_table_ptr`, `DAT_80067840`, `DAT_800676d8`).
2. The same loader reads compressed state into `psx_level_state_compressed_blob` and unpacks through `psx_lzss_unpack_into_level_buffer` into `psx_level_decompressed_state_buffer` (`DAT_8006769c`), then proceeds with root/constructor section dispatch setup.
3. Type runtime banks are installed from subordinate bundle sections: `psx_load_type_state_banks` seeds `psx_type_state_script_bank`, `psx_type_simple_component_bank`, and `psx_type_companion_extents_bank`; descriptor/resource install paths seed `psx_type_art_active_header_bank` and `psx_type_art_built_resource_bank`.
4. Section-0 authored records feed object construction via descriptor-table callback rows. For `0x0042`, both root and constructor families resolve through `0x80063220 -> 0x800626f8` and enter the same generic create/update/release family.
5. During object creation and presentation, runtime art/state ownership is bank-driven, not direct-authored-draw-driven: constructors bind through `psx_type_art_active_header_bank[type]` and `psx_type_art_built_resource_bank[type]`, while draw/update logic also consumes `psx_type_policy_table_ptr[type]` policy bits.
Storage nodes still unresolved (blocking full `map 104` `0x0042` closure):
1. exact schemas and semantics for `DAT_80067938`, `DAT_80067838`, `DAT_80067840`, and `DAT_800676d8` (currently installed by offset in the section pack but weakly typed downstream)
2. map-104-specific ownership/meaning of `psx_level_decompressed_state_buffer` payload slices after inflate (which words materially feed type-`0x0042` object family divergence)
3. explicit per-item runtime correlation for sample set `25/30/31/35/85/86`: bound resource pointer identity, `resource->kind`, and latched frame/state channels against shared `0x0042` descriptor row
4. final control interaction between section-installed policy (`psx_type_policy_table_ptr`), constructor-seeded `obj+0x1c` flags, and any deferred-control reads from subordinate tables before submit
## Latest Object-Local 0x0400 Provenance Pass (2026-04-11)
## Focused Type-Policy Provenance Pass (2026-04-11)
Focused target:
- `DAT_800675f8` / `psx_type_policy_table_ptr`
- concrete map-104 relevance for type `0x0042` and control type `0x0066`
- installation source, reader masks, and storage ownership limits
Functions/addresses inspected in this pass:
- writer/install path:
- `wdl_resource_bundle_load_by_index` (`0x80039444`), write at `0x800398f0`
- key readers sampled directly:
- `psx_object_integrate_motion_and_route_visible` (`0x8001353c`) -> `& 0x1000`
- `psx_draw_main_visible_object` (`0x80041604`) -> `& 0x2000`
- `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`, `0x8002c174`, `0x8002bf5c`) -> `& 0x0600`
- `psx_object_state_machine_dispatch_tick` (`0x8001a280`, `0x8001aa44`) -> `& 0x0800`
- `psx_object_update_nearby_interactions` (`0x8002957c`, `0x80029970`, etc.) -> `& 0x0100`, `& 0x4000`, `& 0x0008`
- `psx_object_register_contact_pair` / related overlap helpers (`0x80028488` family) -> `& 0x0008`
- `psx_find_nearest_los_target_with_typeflag10_or20` (`0x800149cc`, `0x80014b14`) -> `& 0x0010`, `& 0x0020`
- `psx_update_nearest_policy80_contact_marker` (`0x8001408c`) -> `& 0x0080`
Applied live symbol/comment updates (small and evidence-backed):
- rename:
- `FUN_8001408c` -> `psx_update_nearest_policy80_contact_marker`
- comments:
- `0x800398f0`: marks `psx_type_policy_table_ptr` install from level section pack (`DAT_80067838 + local_b4`)
- `0x8002bf0c`: marks `0x0600` class compare role in stage-1 ordering
- `0x80041604`: marks `0x2000` draw-time semitrans gate
- `0x800140c8`: marks `0x0080` policy gate before proximity trace selection
Best evidence on provenance and storage ownership:
1. `psx_type_policy_table_ptr` has exactly one recovered writer in the executable (`0x800398f0`) and is rebuilt during `wdl_resource_bundle_load_by_index`.
2. The installed pointer is computed from the contiguous map bundle section pack (`DAT_80067838 + local_b4`), with subsequent slices derived by offset (`+ local_b0`, `+ local_ac`, `+ local_a8`).
3. This establishes ownership as level-loaded runtime storage (LSET bundle section data), not immutable executable ROM data.
4. Static executable-only recovery of concrete policy rows for type `0x0042` / `0x0066` remains blocked because the table contents are not authored in-place in the code image.
5. Runtime-adjacent cache evidence is still incomplete for these rows: current map-104 sample rows (`25/30/31/35/85/86` for `0x0042`, `53` for `0x0066`) still export `runtimeDiagnostic.typePolicy.sampled=false` and `word=null`.
Current interpretation impact for map-104 `0x0042` art split:
- `psx_type_policy_table_ptr[type]` is now more strongly confirmed as shared per-type policy gating (interaction, ordering, and draw flags), not a per-lane constructor-family selector.
- This table should stay secondary for explaining the `0x0042` `64x64` vs `64x40` split until concrete map-level row capture is available; primary discriminators remain resource identity, pre-latch/latch state flow, and route/state bits.
Focused target:
- route branch inside `psx_object_integrate_motion_and_route_visible`: `obj+0x1c & 0x0400`
- fixed sample families: root `u5=0x0022`, root `u5=0x0030`, constructor `u5=0x0030`
- fixed sample pack: `item:25/30/31/35/85/86`
Concise provenance summary (inspected writer/preserve/transform chains):
- Object-local writers (direct `obj+0x1c` stores):
- `psx_object_create_simple_record` (`0x80024b48`) — copies authored `u5` into `obj+0x1c` at creation (strong object-local seed).
- `psx_object_create_compound_record` (`0x80025040`) — same copy behavior for compound constructors.
- Named mutators examined (mutate low control bits, none introduce direct `0x0400` set/clear):
- `psx_type42_transition_selector_tick`
- `psx_object_select_state_from_transition_table`
- `psx_object_advance_state_script`
- `psx_apply_deferred_control_to_live_objects`
- `psx_object_handle_control_pair_0a`
- Strongest recovered non-object-local `0x0400` writers (nested/runtime or policy-driven):
- `psx_object_state_machine_dispatch_tick` (`0x8001a078`) — nested runtime-state write.
- `psx_object_handle_control_pair_0a` (`0x80022a14`) — global/policy-adjacent write affecting routing control words.
Conclusion: constructors remain the primary object-local seed for `obj+0x1c` (including `0x0400` when authored), but the strongest dynamic `0x0400` writes seen so far are nested/runtime or policy-driven and not direct object-local transforms. A focused live runtime capture on the fixed sample pack is still required to conclusively separate authored `0x0400` from nested/policy-set `0x0400` during the `psx_object_integrate_motion_and_route_visible` branch.
Addresses inspected in this pass:
- route-side consumer and branch context: `0x8001263c..0x80012c2c`, `0x800131a8..0x80013614`
- constructor/local writers: `0x80024b48`, `0x80025040`, `0x80031514`
- transition/mutator lanes touching `obj+0x1c`: `0x8001918c`, `0x800191b0`, `0x800191f4`, `0x8001bde8`, `0x80025dd4`, `0x80025e88`, `0x8002b0c8`, `0x8002b174`, `0x80022b28`
- non-object-local `0x0400` controls in same neighborhood: `0x8001a078`, `0x80022a14`
Recovered chain for object-local `0x0400` in this sample lane:
1. Section-0 authored row feeds constructor.
2. Constructor copies authored `u5` directly into `obj+0x1c` (`psx_object_create_simple_record` / `psx_object_create_compound_record`).
3. Later named mutators in the `0x0042` path rewrite low control bits (`0x0001/0x0002/0x0004/0x0020/0x0040/0x0080/0x0100/0x0200`) but no inspected object-local write site introduces a new `obj+0x1c |= 0x0400` or `obj+0x1c &= ~0x0400` transform.
4. Route split still consumes `obj+0x1c & 0x0400` at `psx_object_integrate_motion_and_route_visible`.
Current strongest read stays unchanged but tighter:
- strongest object-local writer for `0x0400` presence/absence is still authored `u5` copied at object creation
- strongest `0x0400` writer recovered in active runtime logic remains nested/global state, not object-local `obj+0x1c`
Implication for `map 104` `0x0042` remains open but narrower:
- yes, live capture is still needed to close the gap for `item:25/30/31/35/85/86` because static writer recovery still does not pin a direct object-local runtime transformer for bit `0x0400`
## Latest Table Inventory Pass (2026-04-11)
This pass stayed broad on table coverage but concrete on evidence, centered on unresolved `map 104` / type `0x0042` storage and render routing context.
Functions/data inspected in this pass:
- Descriptor and transition tables:
- `0x80063118` (`psx_type_descriptor_table` base)
- `0x80063220` (`psx_type_descriptor_ptr_0042`)
- `0x80063b4c` (`psx_type_transition_selector_rows`)
- Type-policy and runtime-bank globals:
- `0x800675f8` (`psx_type_policy_table_ptr`)
- `0x800758c8` (`psx_type_art_built_resource_bank`)
- `0x800758d0` (`psx_type_simple_component_bank`)
- `0x800758d4` (`psx_type_companion_extents_bank`)
- `0x800758d8` (`psx_type_art_active_header_bank`)
- Marker/runtime control island near post-load reset:
- `0x80063e68` (`DAT_80063e68`)
- `0x80063e54` (`DAT_80063e54`)
- `0x800675ec` (`DAT_800675ec`)
- `0x80067728` (current level index)
- Key functions touched during inventory/decompile:
- `psx_object_create_simple_record`
- `psx_object_create_compound_record`
- `psx_dispatch_section0_dispatch_roots`
- `psx_dispatch_section0_constructor_placements`
- `psx_run_live_object_type_updates`
- `psx_object_select_state_from_transition_table`
- `psx_object_integrate_motion_and_route_visible`
- `psx_draw_main_visible_object`
2026-04-13 15:59:50 +02:00
- `psx_install_type_art_active_header_and_built_resource`
2026-04-12 14:45:08 +02:00
- `psx_level_post_load_runtime_reset`
- `psx_section0_dispatch_root_seed_marker_channel_table`
Applied live symbol updates (small, evidence-backed):
- `0x8002f190` -> `psx_marker_channel_dispatch_mode_action`
- `0x8002f250` -> `psx_marker_channel_runtime_state_clear`
- `0x80031840` -> `psx_marker_channel_mode_is_enabled`
- `0x8003185c` -> `psx_marker_channel_get_mode_step_value`
- `0x80031878` -> `psx_marker_channel_runtime_state_snapshot`
- `0x80031a3c` -> `psx_marker_channel_runtime_state_restore`
Applied targeted comments:
- `0x80063e68`: per-level marker-channel profile seed byte indexed by current level index (`DAT_80067728`) and consumed by seed/apply/reset flow.
- `0x800675ec`: pointer to `0x90`-byte marker-channel runtime state block shared by clear/snapshot/restore/accessor paths.
- `0x80063e54`: per-level companion byte table read by post-load reset and adjacent control helper, likely paired with `DAT_80063e68` as profile/eligibility control.
Concrete table-role clarifications from this pass:
- Type `0x0042` still resolves through generic descriptor table fetch at `psx_type_descriptor_table[0x0042]` (no unique descriptor fork).
- The transition selector row table at `0x80063b4c` remains narrow and state-chooser specific (only direct reads in `psx_object_select_state_from_transition_table`).
- `psx_type_policy_table_ptr` (`0x800675f8`) remains a draw/routing policy source (including semitrans and ordering/publication gating) rather than a unique map-104 art discriminator.
- Runtime-bank trio (`0x800758d0/0x800758d4/0x800758d8`) is confirmed as install/reset-backed loader products consumed by constructors and downstream draw/resource setup.
- The `0x80063e54/0x80063e68/0x800675ec` cluster is now tighter as a level-indexed marker/control runtime island that can affect post-load state and channel behavior, and is therefore a legitimate remaining table family for map-104 type-`0x0042` divergence work.
## Latest Table-Typing Pass (2026-04-11)
This pass stayed focused on turning the newly named storage/control pointers into actual table roles instead of broadening back out into generic render theory.
Applied live symbol updates in this pass:
- `DAT_80067938` -> `psx_ctor_placement_section_ptr`
- `DAT_800676d8` -> `level_clut_table_ptr`
- `DAT_80067840` -> `psx_control_opcode_stream_table`
- `DAT_80063e54` -> `psx_level_selector_table_80063e54`
- `DAT_80063e68` -> `psx_level_channel_table_80063e68`
And the `DAT_800675ec` rename from the previous batch now has a stronger local field map as `psx_marker_channel_runtime_block`.
### `psx_ctor_placement_section_ptr` is a real section-pack root, but not the final row family by itself
The top consumer pair (`wdl_resource_bundle_load_by_index` and `psx_apply_deferred_control_command`) now shows that `psx_ctor_placement_section_ptr` is installed as a section root and then used to derive subordinate bases such as `section_pack_source_80067838`.
The strongest current consumer path is not the original six-halfword constructor row itself. Instead it uses a small u16 index table rooted near `psx_ctor_placement_section_ptr - 2`, multiplies the selected index by `8`, and then walks `8`-byte rows out of the derived `section_pack_source_80067838` base.
So the safe current read is:
- `psx_ctor_placement_section_ptr` is the constructor-placement section root installed at level load
- some downstream control/deferred-command logic uses it as a header/index root for a different `8`-byte row family
- no direct `type == 0x0042` comparison was recovered in this consumer pair yet
That narrows the next storage question again: distinguish the original constructor-placement row family from the subordinate control/index rows derived from the same section root.
### `level_clut_table_ptr` is palette/CLUT support, not the missing `0x0042` art table
The current best evidence from `level_palette_expand_5bit_to_16color` and `level_palette_upload_cluts` is that `level_clut_table_ptr` is a halfword array whose low `5` bits are used as a CLUT index (`0..31`).
That keeps this table on the level-palette side of the pipeline:
- useful for explaining color/CLUT choice
- not a strong candidate for the remaining `0x0042` shape/frame split by itself
So map-104 `0x0042` should still treat `level_clut_table_ptr` as palette support rather than the missing world-object family discriminator.
### `psx_control_opcode_stream_table` is control/state support, not a direct world-art selector
The writer at `0x80039908` and the strongest reader at `psx_control_assign_opcode_stream_by_index` now tighten `DAT_80067840` into a real control/opcode-stream pointer table installed from the level bundle.
Its current best role is:
- primary: state/control opcode stream support for constructor/state-machine callers
- secondary: indirect behavioral influence on presentation through control/state logic
- not a direct art/frame lookup table for unresolved `0x0042`
That means it still matters for map behavior, but it is not currently the best direct explanation for `64x40` versus `64x64` presentation.
### `psx_marker_channel_runtime_block` is now narrowed to marker/control runtime state with two still-interesting short fields
The latest field pass keeps most of this `0x90`-byte block on the marker/control side:
- `+0x34` = mode byte
- `+0x6c` = per-mode/per-step byte
- `+0x88` / `+0x8c` = restored/snapshotted short fields still worth treating as the most presentation-adjacent members in this struct
Snapshot/restore code now clearly reads and writes `+0x88/+0x8c` through global snapshot words before restoring them to the runtime block. That proves they are part of live runtime state, but not yet that they feed ordinary world-object draw.
So this family stays important, but the open question is narrower again: do `+0x88/+0x8c` ever flow into actual render/presentation logic, or do they remain marker/control-only state.
### `psx_level_selector_table_80063e54` and `psx_level_channel_table_80063e68` are per-level control tables
The read windows in `psx_level_post_load_runtime_reset` and `psx_section0_dispatch_root_seed_marker_channel_table` now establish both addresses as per-level byte tables, not scalars.
Current best read:
- `psx_level_channel_table_80063e68` is used by section-0 marker/channel seeding and related dispatch control
- `psx_level_selector_table_80063e54` is a paired per-level selector/index byte consumed in the same post-load/control island
This keeps the whole `0x63e54/0x63e68/0x675ec` family grouped as one legitimate remaining blocker family for level-specific control/state behavior.
### Install-chain consequence
The install-helper pass also tightened the unpack flow one step further:
- `psx_level_decompressed_state_buffer` is still the unpack destination and immediate source for runtime-bank installation
- `psx_load_type_state_banks` remains the installer for `DAT_800758cc/d0/d4`
- `psx_stream_install_type_runtime_banks` remains the packed-stream installer that can populate the art/built-resource side too
- constructors then consume those installed banks directly when binding drawable resources
So the remaining storage/render gap is now tighter than before:
- not “what installs the runtime banks”
- not “is `0x0042` hiding behind one unnamed unique descriptor fork”
- but “which installed rows and control tables still explain the concrete map-104 `0x0042` family split after load”
Highest-value unresolved tables/structures after this pass:
1. `DAT_800675ec` struct layout (`0x90` bytes): fields at `+0x34` bit-flag rows, `+0x6c` step-value rows, and `+0x84..+0x8f` state words still need a named datatype.
2. `DAT_80063e68` and `DAT_80063e54` exact schema (entry count/semantics) and whether they directly separate map-104 `0x0042` families or only gate marker/control channels.
3. Type-`0x0042` runtime-bank interpretation at constructor and post-state-advance points: exact relationship between `DAT_800758d8[type]` active header, `DAT_800758c8[type]` built resource, and effective submitted frame group for sample items `25/30/31/35/85/86`.
4. Per-type policy word capture for `0x0042` in live map-104 sample context (`psx_type_policy_table_ptr[0x0042]`) correlated with stage route (`obj+0x1c` / stage-1 vs stage-2) and bound resource kind.
## Latest Live Cleanup (2026-04-11)
Focused live `SLUS_002.68` cleanup tightened the world-frame/render wrapper lane around
`0x80031f0c`, `0x80031f9c`, `0x800320bc`, `0x80039dc4`, `0x8003977c`, and `0x800391f0`.
Applied in-database symbol cleanups:
- `0x80031e0c` -> `psx_lset_session_loop`
- `0x80031f0c` -> `psx_lset_world_frame_wrapper`
- `0x80031f6c` -> `psx_lset_session_teardown`
- `0x800350a8` -> `psx_render_mode_dispatch`
- `0x80039ef4` -> `psx_level_post_load_runtime_reset`
- `0x80044104` -> `psx_present_frame_and_flip`
Applied direct technical comments at the same lane anchors:
- `0x800320bc`: load-phase sync point between first runtime blob assignment and detached-blob load path.
- `0x800391f0`: per-type state-script bank assignment path.
- `0x8003977c`: per-type descriptor bank assignment path.
- `0x80039dc4`: runtime-header application into active globals.
Operationally this confirms a stable chain for map storage to frame presentation in this lane:
1. `psx_lset_session_loop` drives map-index loads and frame loop control.
2. `lset_level_bundle_load` loads runtime blobs and stream payload lanes.
3. Per-type bank installs (`psx_load_type_state_banks` and descriptor-bank writes) seed runtime object resources.
4. Runtime-header and post-load reset apply mission/level state before steady-state updates.
5. `psx_lset_world_frame_wrapper` dispatches render mode, world visible draw, and `psx_present_frame_and_flip`.
## Latest Concrete Map 104 0x0042 Pass (2026-04-11)
This batch was anchored to concrete `map 104` scene-cache items from scene fingerprint `3497e7f641856415` rather than another broad helper sweep.
Primary sample pack:
- root lane `0x0030`: `item:30` / `item:31` (`raw words: 0042 0b1f 01bf ... 0030`)
- root lane `0x0022`: `item:25` / `item:35` (`raw words: 0042 0bff 01a3 ... 0022`, `0042 0b1f 01df 0002 ... 0022`)
- constructor lane `0x0030`: `item:85` / `item:86` (`raw words: 0042 0b1f/0b3f 019f ... 0030`)
- control constructor sample: `item:53` (`raw words: 0066 0b13 0183 0000 0001 0020`)
Main live symbol recovery in this pass:
- `0x80017fe8` -> `psx_transition_spawn_and_seed_selector_from_record`
- `0x80031044` -> `psx_section0_dispatch_root_find_marker_record_by_channel`
- `0x800460fc` -> `psx_upload_spec_wdl_image_pair_to_vram`
- `0x800463bc` -> `psx_restore_display_draw_env_after_spec_upload`
- `DAT_800675f8` -> `psx_type_policy_table_ptr`
- `DAT_80063a00` -> `psx_type_transition_mode_policy_rows`
- `DAT_80063b4c` -> `psx_type_transition_selector_rows`
### Family bridge is now explicit
- `psx_dispatch_section0_dispatch_roots` is the root-family entry.
- `psx_dispatch_section0_constructor_placements` is the constructor-family entry.
- Both still converge through `psx_type_descriptor_table[0x0042] = 0x80063220 -> 0x800626f8`.
- That shared row still enters `psx_spawn_compound_record_advance_state_once`, then the shared `psx_object_create_compound_record` / `psx_object_advance_state_script` path.
This is stronger negative evidence against any remaining idea that root versus constructor family, or `0x019f` versus `0x01bf`, chooses a different descriptor family for `0x0042`. The practical split the viewer still needs remains later in state, flags, nested runtime state, and resource/frame choice.
### Spawn-side selector seeding is tighter
`psx_transition_spawn_and_seed_selector_from_record` now makes the spawn-side authored selector seed explicit before the later type-specific turn/reseat logic.
- The spawn-side seed feeds `psx_object_select_state_from_transition_table`.
- For `0x0042`, `psx_type42_transition_selector_tick` still remains the stronger explanation for live selector `3/4` cases because those low turn selectors can be emitted before the later latch copy.
So exporter/runtime analysis still needs both channels:
- pre-latch selector evidence
- later latched `obj+0x94`-style state
### Authored lane to `obj+0x1c` handoff is now direct
The constructor-side route-flag story is now more concrete than before.
- Constructors directly copy the authored lane/flags word into `obj+0x1c`.
- Runtime logic then mostly mutates low bits such as `0x0002`; it is not inventing the initial `0x0022` versus `0x0030` split after the fact.
For the current `map 104` sample pack this means:
- lane `0x0022` starts as authored `{ 0x0020, 0x0002 }`
- lane `0x0030` starts as authored `{ 0x0020, 0x0010 }`
That is why `runtimeDiagnostic.objectLocalRouteFlags.initialWord` should be treated as meaningful initial authored state for these records, not only as a later runtime residue.
### Nested `0x0400` still reads stronger than object-local `0x0400`
This pass still did not recover a new direct object-local `obj+0x1c |= 0x0400` writer for `0x0042`.
- The strongest recovered `0x0400` write remains nested-state-side in the wider runtime state machine.
- `0x0042` continues to read and rewrite nested runtime-state words in the same neighborhood.
So the current best read stays split:
- object-local `obj+0x1c` is real authored route/orientation state
- nested runtime state still carries the strongest recovered `0x0400` stage-selection signal
### `DAT_800675f8` is tighter as a level-loaded type-policy pointer
`DAT_800675f8` is now better modeled as `psx_type_policy_table_ptr`.
- It is installed at level load.
- Runtime readers then index it by `type_id << 1` and test the resulting halfword.
- The currently strongest known masks remain:
- `0x1000`: nearby-interaction publication suppression
- `0x0600`: stage-1 ordering class
- `0x2000`: main-visible semitrans policy
That means `runtimeDiagnostic.typePolicy.word` should currently be treated as type-global within a loaded level/session, not as a lane-specific candidate. The remaining gap is the live numeric `0x0042` word for the concrete `map 104` cases, not the table's general role.
### Latest sample-pack follow-up: resource/frame, decode, and routing
The next six-agent concrete pass widened coverage across the same `map 104` anchor pack instead of reopening generic theory.
Additional live renames from this pass:
- `0x80018330` -> `psx_transition_selector_probe_nearby_overlap`
- `0x80018414` -> `psx_transition_selector_probe_marker_overlap`
- `0x800261f4` -> `psx_snapshot_active_object_runtime_rows`
- `0x80025b88` -> `psx_release_all_active_objects_and_reset_type_runtime_banks`
- `0x8002f518` -> `psx_section0_dispatch_root_seed_marker_channel_table`
- `0x80030ee0` -> `psx_section0_dispatch_root_lookup_marker_record_by_kind_channel`
- `0x80030f60` -> `psx_section0_dispatch_root_lookup_marker_slot_by_kind_channel`
- `0x80030fcc` -> `psx_section0_dispatch_root_get_marker_slot_triplet_by_index`
- `0x800311c4` -> `psx_section0_dispatch_root_apply_packed_channel_actions`
- `0x80031454` -> `psx_section0_dispatch_root_spawn_simple_from_marker_record`
Three new practical conclusions matter for the viewer/exporter.
First, the current `0x0042` `64x64` versus `64x40` split still looks more like a shared-resource / different-frame-state problem than a distinct pre-draw resource bind.
- The constructor paths bind type-indexed active-header state before the lane word is copied.
- Draw-time submitter/frame-geometry consumers still take the live frame token from `obj+0x94`.
- So for the current `map 104` sample pack, lane `0x0022` versus `0x0030` is still not strong evidence of a different bound resource by itself.
That keeps the strongest next exporter field recommendation the same in spirit but tighter in scope: preserve a stable bound-resource identity separately from the live frame/state token.
Second, the frame-state bridge is now more explicit end to end.
- `psx_object_select_state_script` installs selector identity into `obj+0x9e`, plus the script cursor and intermediate token state.
- `psx_object_advance_state_script` later latches the live frame/state token into `obj+0x94`.
- Both `psx_project_object_main_visible` and `psx_project_object_special_visible_queue` then consume `obj+0x94` for frame origin/width/height queries before submission.
This sharpens the current exporter rule again: `obj+0x9e` and `obj+0x94` should not be collapsed into one selector field for `0x0042`.
Third, the stage-1 versus stage-2 branch point is now locally explicit even though the exact live `0x0400` provenance for this sample pack is still not closed.
- `psx_object_integrate_motion_and_route_visible` tests `obj+0x1c & 0x0400` at the decisive branch.
- For `0x0042`, the type-`4` special case is irrelevant, so object-local `0x0400` remains the practical stage-2 discriminator.
- The strongest recovered `0x0400` write is still nested-state-side rather than a direct type-`0x0042` object-local OR site.
So the current best routing verdict for the fixed sample pack remains:
- broadly stage-1/main-visible by default
- stage-2 only when object-local `obj+0x1c` actually carries `0x0400` at the routing point
The remaining gap is now very narrow: one live capture pass that samples bound resource identity, `obj+0x9e`, `obj+0x94`, `obj+0x1c`, and the final queue/list path for items `25/30/31/35/85/86`.
### Focused frame-geometry pass: where `64x40` vs `64x64` is chosen (2026-04-11)
This pass started from the now-stable selector/latch bridge and only followed width/height/origin consumers.
Verified bridge and consumer addresses:
- `0x800260e8` `psx_object_select_state_script`: installs selector to `obj+0x9e`
- `0x80025d68` `psx_object_advance_state_script`: latches current frame/state into `obj+0x94`
- `0x80040d44` `psx_project_object_main_visible`: reads `obj+0x94`, then queries frame origin/width/height from `obj+0x10`
- `0x80040f78` `psx_project_object_special_visible_queue`: same `obj+0x94` frame-geometry query path
- `0x8004513c` `psx_resource_frame_origin_x`, `0x800451d0` `psx_resource_frame_origin_y`
- `0x80045014` `psx_resource_frame_width`, `0x800450a8` `psx_resource_frame_height`
Decisive evidence from decompile:
- The projector functions read frame token from `obj+0x94` before any frame-geometry call.
- Width/height/origin helpers branch only on resource kind (`4` sprite-header frame records vs `5` image-table frame records) and use the caller-provided frame index.
- No later projector/draw step rewrites width/height into a `64x40` vs `64x64` family split; later logic applies orientation flip (`obj+0x1c & 0x0002`) and routing/queue behavior, not a replacement size family.
Conclusion for fixed `map 104` `0x0042` sample pack:
- The split is strongest as a live frame-token outcome (`obj+0x94`) within a shared type resource lane, not as a late presentation modifier.
- A resource-header-family branch still exists technically (`kind 4` vs `kind 5` access path), but for same-type `0x0042` objects this is type-level and upstream of per-object frame choice.
Minimum runtime fields to preserve in exporter/viewer for faithful reproduction:
- `obj+0x10` bound resource identity, including resource `kind` (`4` vs `5`)
- `obj+0x94` live latched frame/state token (the geometry key)
- `obj+0x9e` authored selector seed (kept separate from `obj+0x94`)
- `obj+0x1c` route/orientation flags (`0x0002` flip behavior and `0x0400` stage route)
Concrete sample-pack sanity check from current scene cache (`map 104`, fingerprint `3497e7f641856415`):
- item `25` and `35` currently render `64x40` with route seed `0x0022`
- item `30`, `31`, `85`, `86` currently render `64x64` with route seed `0x0030`
This keeps the next unresolved step narrow: capture live bound-resource kind plus live `obj+0x94` for those items to close any remaining uncertainty about per-item frame-token divergence.
## Final Live Map 104 Cohort Pass (2026-04-12)
Pass scope:
- active writable `SLUS_002.68`
- scene fingerprint `3497e7f641856415`
- fixed anchors:
- root `0x0022`: items `25/35`
- root `0x0030`: items `30/31`
- constructor `0x0030`: items `85/86`
- control `0x0066`: item `53`
Functions inspected this pass (create/update/draw + ordering):
- `psx_object_create_simple_record` (`0x80024b48`) - inspected, comment updated
- `psx_object_create_compound_record` (`0x80025040`) - inspected, comment updated
- `psx_object_integrate_motion_and_route_visible` (`0x800131a8`) - inspected, comment updated
- `psx_object_advance_state_script` (`0x80025d68`) - inspected, no edit this pass
- `psx_draw_main_visible_object` (`0x80041458`) - inspected, no edit this pass
- `psx_draw_special_visible_queue` (`0x80041144`) - inspected, no edit this pass
- `psx_main_visible_order_compare_pair_for_graph` (`0x8002bf0c`) - inspected, comment updated
Live artifacts changed in this pass:
- decompiler comments added/updated:
- `0x80024b48`
- `0x80025040`
- `0x800131a8`
- `0x8002bf0c`
Concrete cohort findings from scene-cache fields and executable behavior:
1. Differences that are still real across the fixed map-104 cohorts:
- object-local route seed differs exactly as authored:
- root `0x0022` (`25/35`) -> `initialWord=0x0022` (`bit0x0002=1`, `bit0x0020=1`)
- root/constructor `0x0030` (`30/31/85/86`) -> `initialWord=0x0030` (`bit0x0002=0`, `bit0x0020=1`)
- control `0x0066` sample (`53`) -> `initialWord=0x0020`
- visible frame geometry differs in exported candidates:
- `25/35` -> `64x40`
- `30/31/85/86` -> `64x64`
- `53` -> `64x32`
2. Differences that still fail to differentiate the anchor cohorts:
- stage-route discriminator `bit0x0400` remains clear in all sampled anchors (`routeOutcomeCandidate=main-visible`).
- pre-latch dispatch and latched-state runtime captures remain unsampled (`preLatchDispatchSampled=false`, `latchedState.sampled=false`).
- per-type policy word remains unsampled (`typePolicy.sampled=false`, `word=null`) for both type `0x0042` and control type `0x0066` anchors.
- per-type art-header kind remains unsampled (`resourceKind.sampled=false`), so the kind-4 versus kind-5 split is still unresolved at item granularity.
Strongest executable evidence for the next exporter rule:
- constructors directly preserve authored route/lane word into `obj+0x1c` (`0x80024b48`, `0x80025040`), so `0x0022` versus `0x0030` is a valid authored split signal.
- routing to stage-2 still requires `type==4 || (obj+0x1c & 0x0400)` (`0x800131a8`), and the fixed anchors currently do not satisfy this.
- draw submitter path is resource-kind based and shared by both world-visible lanes (`0x80041458`, `0x80041144`); stage-lane choice alone does not pick sprite versus image-table.
- policy table class bits (`0x0600`) in `0x8002bf0c` are ordering-precedence modifiers, not resource-binding keys.
Practical next exporter rule (highest confidence):
- Keep map-104 `0x0042` cohort partitioning keyed first by authored `record_u5`/`obj+0x1c` seed families (`0x0022` vs `0x0030`) and keep control `0x0066` separate.
- Do not use `typePolicy` or stage-route `0x0400` as primary split keys for these anchors until runtime sampling is populated.
Safe immediate renderer/exporter change suggested by this evidence:
- In scene export/fallback grouping for unresolved map-104 families, add/keep a hard cohort fence:
- `recordRouteFlags.initialWord` must match exactly (`0x0022` group separate from `0x0030`; `0x0066` control separate from `0x0042`).
- Treat `typePolicy.word` and `resourceKind.activeHeaderKindCandidate` as optional tie-breakers only when sampled, never as required keys when null.
- This is safe now because it only prevents cross-cohort over-merge (the repeated-wall failure mode) and does not invent new art mappings.
2026-04-07 17:16:44 +02:00
## Evidence Base
This model is grounded by a combination of static and runtime-adjacent evidence:
- Ghidra decompilation of retail `SLUS_002.68`
- the renderer-local `.cache` scene/export pipeline
- WDL section parsing and bank extraction work
- live comparison between exported scene fields and executable object layouts
- repeated correction of earlier bad hypotheses, especially the disproven "small top-level record stream == whole map" assumption
Most important named functions and data anchors:
- `wdl_resource_bundle_load_by_index`
- `psx_object_create_simple_record`
- `psx_object_create_compound_record`
- `psx_object_select_state_script`
- `psx_object_advance_state_script`
- `psx_object_lookup_variant_entry`
- `psx_object_integrate_motion_and_route_visible`
2026-04-12 14:45:08 +02:00
- `psx_object_update_runtime_input_modes`
2026-04-07 17:16:44 +02:00
- `psx_project_object_main_visible`
- `psx_project_object_special_visible_queue`
- `psx_draw_world_visible_passes`
- `psx_draw_main_visible_object`
- `psx_draw_special_visible_queue`
2026-04-12 14:45:08 +02:00
- `psx_main_visible_list_swap_entries`
- `psx_main_visible_order_graph_unlink_pair`
- `psx_main_visible_order_graph_detach_object`
2026-04-07 17:16:44 +02:00
- `psx_level_root_record_stream` (`DAT_800678f4`)
- `psx_section0_dispatch_root_records` (`DAT_80067720`)
- `psx_section0_constructor_placement_records` (`DAT_800678f0`)
- `psx_type_art_template_bank` (`DAT_800758d8`)
- `psx_type_simple_component_bank` (`DAT_800758d0`)
- `psx_type_state_script_bank` (`DAT_800758cc`)
- `psx_type_companion_extents_bank` (`DAT_800758d4`)
- `psx_level_detached_blob` (`DAT_8006767c`)
- `psx_level_decompressed_state_buffer` (`DAT_8006769c`)
- `psx_level_runtime_header_block` (`DAT_80067794`)
2026-04-12 14:45:08 +02:00
## Resource-Kind Split (Live SLUS_002.68)
The resource creation/submission lane is now explicit enough to treat as stable viewer/exporter evidence.
### Creation and per-type cache
2026-04-13 15:59:50 +02:00
- `psx_install_type_art_active_header_and_built_resource` (`0x80045ffc`) first stores the incoming active header at `DAT_800758d8[type]`, then materializes and caches the drawable resource at `DAT_800758c8[type]`, and temporarily mirrors that built resource back into `DAT_800758d8[type]` until the later header-only override stream restores raw `0x58`-byte headers.
2026-04-12 14:45:08 +02:00
- Exact kind branch in this cache helper:
- `0x80046048`: `kind == 4` -> `psx_resource_bind_single_image_vram_slot` (`0x800444e4`)
- `0x80046054`: `kind == 5` -> allocate bundle wrapper and call `image_bundle_load_to_vram` (`0x80044614`)
- `psx_create_image_resource_from_descriptor` (`0x80044434`) uses the same discriminator:
- `0x8004445c`: `kind == 4` branch
- `0x80044468`: `kind == 5` branch
- `0x8004447c`: call `psx_resource_bind_single_image_vram_slot`
- `0x800444ac`: call `image_bundle_load_to_vram`
### Draw-time submitter selection
- `psx_draw_main_visible_object`:
- `0x800415a8`: compare `resource->kind` against `5`
- `0x800415c0`: `kind == 5` -> `psx_image_table_submit_frame` (`0x80044e9c`)
- `0x800415e0`: otherwise -> `psx_sprite_resource_submit_frame` (`0x80044bdc`)
- `psx_draw_special_visible_queue`:
- `0x800412c8`: compare `resource->kind` against `5`
- `0x800412dc`: `kind == 5` -> `psx_image_table_submit_frame`
- `0x800412f8`: otherwise -> `psx_sprite_resource_submit_frame`
- `psx_draw_hud_overlay_pass` has the same split for slot-managed overlays:
- `0x80041b5c`: image-table submit path
- `0x80041b84`: sprite submit path
### Overlay-lane exception
The HUD/overlay lane now has one important exception to the otherwise clean world-object rule.
- `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` branch directly on `resource->kind == 5`.
- `psx_draw_hud_overlay_pass` can route through image-table submission from an overlay-slot flag (`0x10`) rather than by treating the slot resource like a normal world-object kind check.
- `psx_draw_clock_digits_overlay` is a fixed image-table user, not evidence for world-object family binding.
So the viewer/exporter should reuse resource-kind logic for world-object lanes, but it should not mine the HUD/overlay lane as if it were one more map-facing art discriminator.
### Runtime implication for viewer binding
- Treat `resource kind == 5` as table-backed UV/frame metadata (`psx_image_table_submit_frame`) and `resource kind != 5` as streamed/decoded sprite uploads (`psx_sprite_resource_submit_frame`).
- Keep lane semantics separate:
- main-visible can OR authored palette high-byte overrides into submit flags
- special-visible submits without that authored override OR path
- HUD/overlay has its own slot/table policy and should not be used as a donor for world-object map art binding.
2026-04-07 17:16:44 +02:00
## How Map Data Is Stored
### 1. `LSET*.WDL` is a multi-section bundle
The executable-backed loader model is no longer speculative:
- `wdl_resource_bundle_load_by_index` opens `SPEC_A.WDL` and the selected `LSET*.WDL`
- it reads a `0x38` header whose first nine dwords act as section sizes
- it then lays out multiple runtime pointers rather than one monolithic map blob
Current map-relevant runtime destinations:
- `psx_level_root_record_stream` (`DAT_800678f4`): top-level root record stream
- `psx_section0_dispatch_root_records` (`DAT_80067720`): secondary `0x18`-stride authored record family
- `psx_section0_constructor_placement_records` (`DAT_800678f0`): `0x0c`-stride constructor-placement family
- `psx_type_art_template_bank` (`DAT_800758d8`): per-type art/template descriptor bank
- `psx_type_simple_component_bank` (`DAT_800758d0`): per-type simple-record payload bank
- `psx_type_state_script_bank` (`DAT_800758cc`): per-type state-script bank
- `psx_type_companion_extents_bank` (`DAT_800758d4`): per-type variant/companion bank
- `DAT_800675f8`: per-type flags table
- `psx_level_detached_blob` (`DAT_8006767c`): additional detached level blob
- `DAT_8006b5d8 -> psx_level_decompressed_state_buffer` (`DAT_8006769c`): optional decompressed `0x3e00` runtime/state substrate
- `psx_level_runtime_header_block` (`DAT_80067794`): separate `0x50` level runtime-header block
2026-04-12 14:45:08 +02:00
The loader/install ownership is now tighter too:
- `psx_load_type_state_banks` installs the `DAT_800758cc/d0/d4` banks only.
- `psx_stream_install_type_runtime_banks` is the packed-stream helper that can install all four per-type banks (`DAT_800758cc/d0/d4/d8`) together and clears the cached owner/resource slot at `DAT_800758c8`.
- `psx_snapshot_level_runtime_header_block` and `psx_apply_level_runtime_header_block` make the `DAT_80067794` lane read as save or transition state, not as the missing per-type art-binding source for unresolved `0x0042` families.
2026-04-07 17:16:44 +02:00
### 2. The map is split across authored families, not one row type
The viewer's old `region00/region01` labels were a useful stepping stone, but the current executable-backed names are better:
- `section0_dispatch_roots`: closest to the `DAT_80067720` / root-dispatch family
- `section0_constructor_placements`: closest to the `DAT_800678f0` constructor-input family
These runtime anchors are now named the same way in the live Ghidra database:
- `section0_dispatch_roots` aligns with `psx_section0_dispatch_root_records`
- `section0_constructor_placements` aligns with `psx_section0_constructor_placement_records`
These families do different jobs.
`section0_dispatch_roots`:
- generic runtime-object descriptors
- fed into per-type dispatch handlers
- not yet directly final render primitives
- include families that still need downstream runtime state/variant logic before visible art is known
`section0_constructor_placements`:
- tighter constructor-facing rows
- much closer to direct object spawn inputs
- already usable for a large part of the viewer export
2026-04-12 14:45:08 +02:00
### 3. The descriptor-table cluster for `0x003e..0x0050` is shared
The newest Ghidra pass tightens the per-type descriptor story substantially.
- `psx_type_descriptor_table` at `0x80063118` is a pointer table.
- Every currently sampled type in the `0x003e..0x0050` band points to the same descriptor object at `0x800626f8`.
- Type `0x0042` is now pinned exactly too: `psx_type_descriptor_table[0x0042]` at `0x80063220` points to `0x800626f8`.
- That shared descriptor currently resolves to a small callback cluster that includes:
- `psx_spawn_compound_record_advance_state_once`
- `psx_object_refresh_main_visible_and_cleanup`
- `psx_object_release_to_free_list`
- `psx_spawn_simple_record_set_active_flag`
- `psx_object_advance_state_and_queue_special_visible`
- `psx_object_create_simple_record`
- `psx_object_integrate_motion_and_route_visible`
This is important negative evidence. `0x0042` does not currently have a unique descriptor fork that would justify a type-only exporter key. If the viewer needs to distinguish `0x0042` from neighboring generic-family types, that distinction has to be recovered later from runtime-bank content, state progression, flags, lane routing, or resource-kind evidence.
The row layout is now clearer too:
- `descriptor + 0x00`: section-0 create/dispatch callback
- `descriptor + 0x04`: per-object update callback copied into the live object
- `descriptor + 0x08`: release callback
- `descriptor + 0x0c`: descriptor flags
The newest pass also tightens the strongest known constructor-placement route for `0x0042` itself:
- `psx_spawn_compound_record_advance_state_once`
- `psx_object_create_compound_record`
- `psx_object_advance_state_script`
- `psx_object_refresh_main_visible_and_cleanup`
- stage-1 main-visible draw through `psx_draw_main_visible_object`
That is useful negative evidence too. Constructor-placement `0x0042` currently reads as one compound/main-visible path inside the generic descriptor family, not as a special queue or a hidden presentation-only lane.
### 4. The late template bank matters
2026-04-07 17:16:44 +02:00
The per-type art bank in `DAT_800758d8` is not taken from the earlier small-section heuristic.
Current best read:
- the useful late `DAT_800758d8` candidate sits in a late large section
- it decodes only when that section is treated as a bank with an embedded `+0x38` parse start
- on retail map `9`, that correction lifts resolved bundle-mapped items from `0` to `111`
This is one of the strongest pieces of evidence that the viewer must respect executable loader structure rather than broad file-wide scans or first-match heuristics.
2026-04-12 14:45:08 +02:00
The art-cache side is narrower now too. `psx_type_art_template_bank` and `psx_type_art_resource_cache_bank` no longer read as a simple immutable descriptor-versus-resource split:
- the cache-build path first seeds `psx_type_art_template_bank[type]` with the incoming descriptor/header pointer
- after resource construction, the helper writes the built resource pointer back to both `psx_type_art_resource_cache_bank[type]` and `psx_type_art_template_bank[type]`
- constructor-side consumers then treat `psx_type_art_template_bank[type]` as the active art header used for kind discrimination, while `psx_type_art_resource_cache_bank[type]` acts as the reusable per-type built-resource cache
That still does not create a unique `0x0042` branch, but it does mean exporter notes should treat the `DAT_800758d8` / `DAT_800758c8` pair as active per-type art state, not as one permanently raw descriptor table plus one completely separate cache.
The current live database names now reflect that tighter read too:
- `DAT_800758d8` = `psx_type_art_active_header_bank`
- `DAT_800758c8` = `psx_type_art_built_resource_bank`
2026-04-07 17:16:44 +02:00
## Constructor Record Layouts
Two constructor families are now strong enough to describe directly.
### Compound-record constructor
`psx_object_create_compound_record` reads:
- `type` from `record + 0x00`
- `x` from `u16` at `+0x02`
- `y` from `u16` at `+0x04`
- `z` from byte `+0x06`
- initial state selector from byte `+0x08`
- flags from `+0x0a`
### Simple-record constructor
`psx_object_create_simple_record` reads:
- `type` from `record + 0x04`
- `x` from `u16` at `+0x08`
- `y` from `u16` at `+0x0a`
- `z` from byte `+0x0c`
- initial state selector from byte `+0x0e`
### Common constructor outputs
Both constructors:
- write authored coordinates into object fields `+0x3c/+0x40/+0x44` as `16.16` fixed-point
- preserve the original authored source-record pointer at `obj + 0xa0`
2026-04-12 14:45:08 +02:00
- copy the authored lane/flags word into `obj + 0x1c`
2026-04-07 17:16:44 +02:00
- resolve the per-type art bank and seed a drawable resource pointer at `obj + 0x10`
- store the per-type variant bank at `obj + 0x84`
- store the per-type state-script bank at `obj + 0x88`
- call `psx_object_select_state_script` to seed the initial live state
That preserved source-record pointer at `obj + 0xa0` is especially important because it closes the palette-override provenance: later draw code really is reading authored bytes directly.
2026-04-12 14:45:08 +02:00
Current best read for the copied lane/flags word is:
- bit `0x0020`: broad world-visible route gate
- bit `0x0002`: orientation/extents-axis swap and projected horizontal-anchor flip, not a lane switch
- bit `0x0400`: per-instance stage selector later consumed by visible-routing helpers
2026-04-07 17:16:44 +02:00
## Runtime Banks And Object Fields
The current best object-centric map/render model revolves around a small cluster of object fields.
### Per-type banks
- `psx_type_art_template_bank` (`DAT_800758d8`): art/template descriptor bank
- `psx_type_simple_component_bank` (`DAT_800758d0`): simple-record local payload bank
- `psx_type_state_script_bank` (`DAT_800758cc`): state-script bank
- `psx_type_companion_extents_bank` (`DAT_800758d4`): variant/companion bank
### Important object fields
- `obj + 0x10`: current drawable resource pointer
2026-04-12 14:45:08 +02:00
- `obj + 0x1c`: live route/flags word copied from the authored row and later consumed by visible-routing helpers
2026-04-07 17:16:44 +02:00
- `obj + 0x20..0x2e`: projected on-screen rectangle
- `obj + 0x3c/+0x40/+0x44`: fixed-point world `x/y/z`
- `obj + 0x54/+0x58/+0x5c`: next/target world position cluster used by motion/integration helpers
- `obj + 0x60/+0x64/+0x68`: motion vector used by heading/state reselection helpers
- `obj + 0x78/+0x7c`: intermediate projected screen anchor in fixed-point
- `obj + 0x84`: current variant bank pointer
- `obj + 0x88`: current state-script table pointer
- `obj + 0x8c/+0x90`: active script base and current script cursor
- `obj + 0x94`: current script word, which is already the live frame/state index used by later draw helpers
- `obj + 0x9e`: original authored selector stored by `psx_object_select_state_script`
- `obj + 0xa0`: original authored source-record pointer
The crucial distinction is:
- `obj + 0x9e` is the authored input selector
- `obj + 0x94` is the current live script word after advancement/reselection
The executable's later art-facing logic follows `obj + 0x94`, not `obj + 0x9e`.
## State Scripts, Variants, And Why The Map Is Still Unreadable
### 1. Initial state selection is not final state selection
`psx_object_select_state_script`:
- stores the authored selector at `obj + 0x9e`
- chooses an initial script base from `DAT_800758cc`
- seeds `obj + 0x8c/+0x90`
But that is only the starting point.
### 2. The runtime advances and sometimes reseats the live script
`psx_object_advance_state_script`:
- interprets sentinel-driven script records
- updates `obj + 0x94` from the current script word
- reruns `psx_object_lookup_variant_entry`
Verified sentinel/control behavior now includes:
2026-04-12 14:45:08 +02:00
- `0xfffe`: `psx_script_dispatch_audio_event`, a non-visible audio/sequence side-effect dispatch
2026-04-07 17:16:44 +02:00
- `0xfffd`: direct in-family jump
- `0xfffc`: immediate switch to subsidiary script-table entry
- `0xfffb`: scan-forward variant that consumes the next in-band `0xfffd` selector before switching
### 3. Variant lookup is indexed by live state, not by raw placement selector
`psx_object_lookup_variant_entry`:
- reads `DAT_800758d4`
- indexes it by `obj + 0x94`
- does not index directly by `obj + 0x9e`
That is the key split between authored placement metadata and runtime-visible state.
### 4. The current `DAT_800758d4` evidence points to companion extents, not final art
The newly traced consumer path is narrower and more concrete than the earlier placeholder-art theory.
`psx_object_advance_state_script`:
- reruns `psx_object_lookup_variant_entry`
- sign-extends the returned three bytes into `obj + 0x30/+0x34/+0x38`
- does not update `obj + 0x10`
- does not replace the live frame index stored at `obj + 0x94`
Downstream consumers of `obj + 0x30/+0x34/+0x38` are now verified in the interaction lane:
- `psx_object_test_overlap_3d` uses those fields as the object's overlap extents against `obj + 0x54/+0x58/+0x5c`
- `psx_object_update_contact_block_flags` uses the same extents while setting directional block/contact bits
- `psx_object_reselect_state_from_target_vector` and `psx_type4_reselect_motion_state` use target-object `+0x30/+0x34/+0x38` as target bounds while reseating heading-based state
By contrast, the visible projectors and draw helpers still take visible art only from:
- the drawable resource pointer at `obj + 0x10`
- the live frame/state word at `obj + 0x94`
So `DAT_800758d4` is currently better described as a per-state companion-extents bank than as the last missing direct art table.
### 5. Interaction and heading state can rewrite the live script
The runtime does not only advance scripts linearly.
Verified reselection path:
- `psx_object_reselect_state_from_target_vector`
- `psx_object_quantize_motion_heading16`
- `psx_quantize_vector_heading16`
2026-04-12 14:45:08 +02:00
- `psx_object_select_state_from_transition_table`
- `psx_type42_transition_selector_tick`
The live reselection path is now slightly tighter than before:
- `psx_heading16_lookup_unit_vector` is the table-backed heading-to-vector helper used by `psx_object_reselect_state_from_target_vector` when a target-side heading token is available
2026-04-07 17:16:44 +02:00
Verified interaction/reselection cluster:
- `psx_type4_update_delayed_interaction`
- `psx_type4_reselect_motion_state`
- `psx_object_update_nearby_interactions`
- `psx_object_test_overlap_3d`
- `psx_object_update_contact_block_flags`
- `psx_object_register_contact_pair`
2026-04-12 14:45:08 +02:00
Latest live `SLUS_002.68` cleanup in this same cluster also closes six previously anonymous helpers that shape post-spawn interaction state:
- `0x80028050` = `psx_object_test_strict_nonoverlap_flag8_pair`
- `0x800281d4` = `psx_object_test_strict_nonoverlap_flag8_subject`
- `0x80028700` = `psx_object_adjust_param9c_by_view_side`
- `0x800287bc` = `psx_object_update_param9c_from_contact_target`
- `0x80028eb4` = `psx_object_apply_contact_push_bias`
- `0x8002923c` = `psx_object_spawn_type11_contact_proxy`
- `0x8001ae9c` = `psx_object_update_interaction_transition`
- `0x8001bca0` = `psx_object_select_state_from_transition_table`
Current practical read for exporter-facing behavior:
- `+0x30/+0x34/+0x38` remains the shared runtime companion-extents lane used by overlap/reselection checks.
- `+0x9c` is actively rewritten after contact/reselection and camera-side scaling, so it should not be treated as spawn-static metadata.
- contact-triggered type-`0x11` proxy spawning now has a direct helper anchor (`0x8002923c`) inside nearby-interaction flow, not just generic script-advance assumptions.
- the transition-table lane is now explicit too: `psx_object_select_state_from_transition_table` uses the per-type table at `DAT_80063b4c` plus heading-bucket logic to choose selectors before `psx_object_select_state_script` reseats the active script.
- `psx_object_update_interaction_transition` shows one concrete forced-selector path: spawned helper objects are pushed to selector `3` unless they are already in selector `1` or `3`.
- type `0x0042` now has a dedicated transition/update helper too: `psx_type42_transition_selector_tick` computes and dispatches low turning selectors before the `+0x94`-style runtime latch copy.
- for type `0x0042`, the `DAT_80063b4c` row itself mostly yields higher script selectors; the low selectors `3/4` are better explained by that dedicated pre-latch turn/reseat path than by the row alone.
- the constructor-facing sample path now also closes one adjacent helper: `psx_spawn_compound_transition_effect_by_code` is a transition-effect constructor helper used in the same `0x0042` transition lane and confirms that selector install writes `obj + 0x9e` while the later state advance still owns the `obj + 0x94` latch.
2026-04-07 17:16:44 +02:00
These helpers prove that post-spawn interaction and motion state can reseat the active script from runtime heading, not only from the authored row.
That is why exported `0x0042` selectors `3` and `4` do not contradict an earlier three-script file read: some of those live selectors are runtime outcomes.
2026-04-12 14:45:08 +02:00
The latest pass narrows that one step further for `0x0042`: selector `3/4` can be dispatched before the `+0x94`-style runtime latch is updated. So exporter logic that keys too literally off the current latched script word can still miss a just-selected turn state.
The pre-latch path is now slightly tighter again:
- `psx_type42_transition_selector_tick` uses `psx_object_is_within_view_margin` as an early gate.
- it can take a heading bucket from `psx_object_compute_heading_selector_to_focus`, remap that bucket through mirrored-turn logic, and call `psx_object_select_state_script` before the latch copy.
- only after that dispatch does the helper copy its transition result into the `+0x94`-style runtime latch word.
The newest cache evidence also shows the remaining `0x0042` failure is not one homogeneous placement class. On `map 104`, the same donor binding currently spans:
- `section0_constructor_placements` with `u5=0x0020`
- `section0_dispatch_roots` with a large `u5=0x0030` band
- smaller `section0_dispatch_roots` outliers with `u5=0x0022`
In the current cache all of those still land on donor map `85` type `0x0040` bundle `0x0009d304` with palette `0`. That is useful as negative evidence: the exporter is currently flattening multiple authored/runtime roles into one wall-like resource before the executable-backed family split is proven.
The viewer now carries a narrow safety rule derived from that evidence. Provisional donor matches in the unresolved generic family are rejected when one `map:type` bucket spans mixed authored families or mixed raw `u5` classes. That does not solve the missing runtime resource rule, but it does stop the exporter from presenting a single false wall field as if the executable had already proven it.
The same six-track verification pass also narrows one part of the `u5` split. For root-dispatch `0x0042`, the smaller `u5=0x0022` cases do not currently read as a separate runtime lane from the dominant `u5=0x0030` band. Both still satisfy the same broad main-visible gating through bit `0x0020`; the visible difference is that `0x0022` additionally carries bit `0x0002`, which affects orientation/extents math and therefore looks more like a presentation variant than a separate queue or subsystem path. So the exporter should keep `u5` visible and distinct, but it should not assume every `0x0022` / `0x0030` split implies a different draw pass.
2026-04-07 17:16:44 +02:00
### 6. This is the current blocker
The map is still unreadable because the viewer still does not fully reproduce that last runtime bridge:
- authored row
- initial state bank entry
- post-spawn script advancement/reselection
- live companion-extents lookup
- final visible resource/frame choice
Projection, placement decoding, list routing, and primitive submission are no longer the main unknowns.
2026-04-12 14:45:08 +02:00
One exporter-side step is now in place to make the next runtime pass more concrete. The PSX scene builder now emits a per-item `runtimeDiagnostic` payload in scene version `psx-runtime-record-probe-v10`.
Current diagnostic channels exported for each PSX item:
- `objectLocalRouteFlags`: seeded from raw `u5`, with decoded `0x0002` / `0x0020` / `0x0200` / `0x0400` bits and a route-outcome candidate
- `selector`: raw selector seed plus an explicit note when the type-`0x0042` pre-latch turn path may diverge from the later latch
- `latchedState`: current exporter-side state/frame candidate used for scene art selection
- `nestedRuntimeState`: explicit placeholder slots for the live nested runtime words that still need Ghidra-side sampling
- `resourceKind`: per-type active-header/built-resource hints derived from the current art-bank decode
- `typePolicy`: explicit placeholder slot for the live `DAT_800675f8` word
That payload does not solve the remaining `0x0042` bridge by itself, but it gives the next Ghidra pass a stable schema to fill against concrete scene items instead of re-deriving which channels matter.
2026-04-07 17:16:44 +02:00
## Per-Frame World And Render Pipeline
### 1. Outer lifecycle
- `psx_level_session_loop`: outer level-session loop
- `wdl_resource_bundle_load_by_index`: level load and runtime-bank setup
- `psx_world_frame_tick`: normal per-frame world loop
- `psx_draw_world_visible_passes`: top-level draw submission
### 2. Authored record dispatch before live-object draw
The executable still operates on authored families each frame before or alongside live objects:
- `psx_dispatch_section0_dispatch_roots`: dispatches the `DAT_80067720` family plus nearby fixed-size entries
- `psx_dispatch_section0_constructor_placements`: dispatches the `DAT_800678f0` constructor-placement family
2026-04-12 14:45:08 +02:00
- `psx_authored_record_in_view_bounds`: shared screen-space cull gate for those authored record families before handler dispatch
2026-04-07 17:16:44 +02:00
These are the closest executable matches for the viewer's exported authored record families.
2026-04-12 14:45:08 +02:00
That distinction matters directly for the current renderer bug. The active `map 104` cache regression is no longer just “many `0x0042` records choose the wrong wall art”; it is “records emitted from both of these authored families are currently being funneled into the same donor resource despite different raw `u5` classes.” So the next exporter repair should preserve family identity until runtime evidence proves a shared resource path.
2026-04-07 17:16:44 +02:00
### 3. Live-object update lane
- `psx_run_live_object_type_updates`: per-type live-object update callback pass
- `psx_run_live_object_behavior_callbacks`: later per-object behavior callback pass
- `psx_object_integrate_motion_and_route_visible`: integrates motion, updates visibility flags, advances script state, and routes the object to the appropriate render lane
2026-04-12 14:45:08 +02:00
That lane now has two more verified helper clusters that matter for map reconstruction:
- `psx_run_object_behavior_program_tick` and `psx_object_behavior_opcode_dispatch` service a small per-object timed opcode stream before or alongside the main motion/update path
- `psx_world_point_in_view_bounds` is the shared world-space cull helper used both by `psx_object_integrate_motion_and_route_visible` and `psx_run_live_object_type_updates`
- `psx_object_run_control_opcode`, `psx_control_move_player_to_point`, `psx_control_move_object_to_point`, `psx_control_wait_ticks`, `psx_control_configure_fixed_camera_anchor`, `psx_control_set_facing_direction`, `psx_queue_deferred_control_command`, `psx_flush_deferred_control_queue`, `psx_apply_deferred_control_command`, `psx_apply_deferred_control_to_dispatch_roots`, and `psx_apply_deferred_control_to_live_objects` form a small control-script lane that mutates both per-object motion state and deferred world control state during the world/update step rather than during final draw
The old unnamed post-projection `FUN_80027f80` follow-up is now closed too. It is not a hidden render cleanup path. The live helpers are:
- `psx_reset_nearby_interaction_list`
- `psx_nearby_interaction_list_add`
- `psx_nearby_interaction_list_remove`
- `psx_update_motion_and_nearby_interactions`
Current best read:
- `psx_object_integrate_motion_and_route_visible` and `psx_object_refresh_main_visible_and_cleanup` enqueue eligible objects into a nearby-interaction active set after projection/state refresh
- `psx_update_motion_and_nearby_interactions` consumes that active set before the next frame, running `psx_type4_update_delayed_interaction` for type `4` objects and `psx_object_update_nearby_interactions` for the broader collision/contact lane
- `psx_control_move_player_to_point` and `psx_control_move_object_to_point` close control opcode case `1` as a move-to-point instruction rather than a render-side helper, `psx_control_wait_ticks` closes case `3` as a timed gate on `DAT_80078a28`, `psx_control_configure_fixed_camera_anchor` closes cases `4/5` as the fixed-camera-anchor configurator, and `psx_control_set_facing_direction` closes case `9` as an explicit heading override
- control opcode case `8` is still not named at the wrapper level, but its direct callee is now grounded: `psx_spawn_object_compound_effect_variant3` creates a type-`2` compound effect at the current object position, so the wrapper currently reads as a short delay gate around that spawn plus a local motion-state change
- the separate deferred control-command queue is world-facing control state, not a hidden presentation queue; it is flushed from the per-frame world loop before draw submission and can touch both section-0 dispatch rows and instantiated live objects
- this queue is therefore part of runtime interaction maintenance, not a separate hidden art-routing pass
2026-04-07 17:16:44 +02:00
### 4. Stage 1 versus stage 2 is a real runtime split
2026-04-12 14:45:08 +02:00
The split point is now explicit inside `psx_object_integrate_motion_and_route_visible`, not just inferable from later draw helpers.
- `obj + 0x1c` bit `0x0020` keeps the object in the broad world-visible route class.
- `obj + 0x1c` bit `0x0400` selects the stage-2 path: set routes to `psx_project_object_special_visible_queue`, clear falls through to `psx_project_object_main_visible`.
- `obj + 0x1c` bit `0x0002` still reads as orientation/extents behavior only; it does not switch lanes by itself.
- `DAT_800675f8[type]` bit `0x1000` gates the nearby-interaction publish call after projection, and bits `0x0600` feed the stage-1 order-graph comparator.
The writer side is now tighter too:
- constructors seed `obj + 0x1c` directly from the authored `u5` word
- `psx_object_select_state_from_transition_table` can set or clear bit `0x0002`
- the strongest recovered stage-2 consumer remains the route test itself; no equally strong local writer for bit `0x0400` has been confirmed yet in the named helper set, so current best read is still that `0x0400` is usually data-driven from authored state unless one of the remaining anonymous control islands proves otherwise
The anonymous control islands now partially close that last caveat.
- `psx_object_state_machine_dispatch_tick` contains a confirmed write of bit `0x0400`, but to the nested runtime state word, not directly to the object-local `obj + 0x1c` halfword.
- `psx_object_handle_control_pair_0a` can set global policy bits in `DAT_80078a88`, clear the nested runtime `+0x1c` dword, and set object-local bit `0x0200`, but this pass still did not recover a direct object-local `obj + 0x1c |= 0x0400` write.
So the current best read stays split: `0x0400` is definitely a real stage-selection concept in the wider runtime state machine, but a direct object-local `0x0042` writer is still not pinned in the named helper set.
2026-04-07 17:16:44 +02:00
Stage 1:
- projector: `psx_project_object_main_visible`
- draw helper: `psx_draw_main_visible_object`
- object membership stored in `psx_main_visible_list` (`DAT_8006ad5c`)
Stage 2:
- projector: `psx_project_object_special_visible_queue`
- draw helper: `psx_draw_special_visible_queue`
- queue stored in `psx_special_visible_queue` (`DAT_80078b70`) with count `psx_special_visible_queue_count` (`DAT_80067472`)
- reset each frame by `psx_reset_special_visible_queue`
This is not a HUD-only split. Stage 2 is a real world-facing lane.
2026-04-12 14:45:08 +02:00
The type-flag lane is now more concrete too:
- `DAT_800675f8[type] & 0x1000`: suppresses nearby-interaction publication after the stage-2 route point
- `DAT_800675f8[type] & 0x0600`: stage-1 ordering priority class used by the order graph and slice sorter
- `DAT_800675f8[type] & 0x2000`: main-visible semitrans policy bit at draw time
Those flags now read as ordering, interaction, and draw-policy modifiers. They do not currently overturn the stronger route split recovered from `obj + 0x1c`.
The type-flag lane is also broader than the first pass suggested, but still policy-only:
- bit `0x0800`: reduced or half-adjust behavior in the radius/param9c decay family
- bits `0x0010` / `0x0020`: target-class selection used by nearby LOS-target search helpers
- bit `0x0008`: flag-8 target-family inclusion gate in contact and decay helpers
These sharpen neighboring gameplay semantics, but they still do not create a unique `0x0042` presentation lane by themselves.
The renamed tail of the top-level draw pass is now clearer too:
- `psx_draw_world_visible_passes` ends with `psx_draw_hud_overlay_pass` (`0x800416cc`), not a third world-object lane.
- One concrete child of that pass is `psx_draw_clock_digits_overlay` (`0x8004214c`), which formats and submits the timer/clock digits through the image-table submitter.
The adjacent presentation-only helper chain is now named in the live database:
- `psx_level_session_loop` calls `psx_hud_overlay_init_resources` (`0x800388a8`) during level-session bring-up.
- `psx_hud_overlay_init_resources` preloads fixed HUD resources and descriptor defaults used by the later overlay pass.
- `psx_draw_hud_overlay_pass` consumes the slot table and calls `psx_overlay_slot_step_color_fade` (`0x80038114`) per active slot.
- overlay/menu controllers allocate and retire those slots through `psx_overlay_slot_create` (`0x80035cc0`) and `psx_overlay_slot_release` (`0x80036000`).
This chain is a non-map-facing presentation lane: it is layered after both world-visible passes and does not participate in authored map record dispatch, world projection, or stage-1/stage-2 world list routing.
2026-04-07 17:16:44 +02:00
### 5. Small wrapper helpers prove the split is intentional
Recovered wrappers:
- `psx_spawn_compound_record_advance_state_once`
- `psx_spawn_simple_record_set_active_flag`
- `psx_object_refresh_main_visible_and_cleanup`
- `psx_object_advance_state_and_queue_special_visible`
These wrappers show that the executable contains dedicated short-form helpers for:
- spawn then immediately advance state
- spawn then mark active
- refresh and project into stage 1
- advance state and immediately queue into stage 2
That is strong evidence that route selection is a first-class runtime decision, not an incidental byproduct of the draw code.
## Projection Model
The PSX executable-backed coordinate model is now stable enough for viewer use.
### World coordinates
Authored world values are stored in object fields:
- `obj + 0x3c`: `x` as `16.16`
- `obj + 0x40`: `y` as `16.16`
- `obj + 0x44`: `z` as `16.16`
### Projection
`psx_project_object_main_visible` and `psx_project_object_special_visible_queue` use:
- `screen_anchor_x = y - x`
- `screen_anchor_y = 2*z - (x + y)/2`
They write the fixed-point intermediate anchor to:
- `obj + 0x78`
- `obj + 0x7c`
Then they subtract camera origin and per-frame origin metrics to produce the final on-screen rectangle at:
- `obj + 0x20..0x2e`
This matters for the viewer because PSX sprites should be positioned from those authored screen rectangles, not from a DOS-style reconstructed wireframe.
## Render Submission Path
### 1. Top-level draw pass
`psx_draw_world_visible_passes`:
1. asks `psx_main_visible_list_get_sorted_slice` for the current sorted stage-1 slice
2. iterates it through `psx_draw_main_visible_object`
3. then draws stage 2 through `psx_draw_special_visible_queue`
4. then executes the HUD/overlay pass
### 2. Stage-1 visible-list management
Named helpers:
- `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`
- `psx_main_visible_list_get_sorted_slice`
The sorter is important because it shows that draw order is not just screen-y or type-only ordering. It uses dependency and tie-break logic tied to the `DAT_800915xx` ordering data plus depth/position rules.
### 3. Frame metrics and final resource-specific submitters
Frame metric accessors:
- `psx_resource_frame_origin_x`
2026-04-12 14:45:08 +02:00
The final submitter split is now explicit:
- both `psx_draw_main_visible_object` and `psx_draw_special_visible_queue` choose the submitter from the bound resource header at `obj + 0x10`, not from type id alone
- if `resource->kind == 5`, draw goes through `psx_image_table_submit_frame`
- otherwise draw goes through `psx_sprite_resource_submit_frame`
That means image-table-versus-sprite submission is a runtime resource-kind property, not a stable type-family label. For unresolved `0x0042`, this is one of the last meaningful missing facts: the decision point is now known exactly, even though the per-map resource-header kind still has to be sampled from runtime-loaded art-bank data.
2026-04-07 17:16:44 +02:00
- `psx_resource_frame_origin_y`
- `psx_resource_frame_width`
- `psx_resource_frame_height`
Resource-specific submitters:
- `psx_sprite_resource_submit_frame`
- `psx_image_table_submit_frame`
The critical point is that both paths take the live frame index from `obj + 0x94`.
### 4. Resource creation path
`psx_create_image_resource_from_descriptor`:
2026-04-12 14:45:08 +02:00
- type-4 descriptors bind a single image resource through `psx_resource_bind_single_image_vram_slot`
2026-04-07 17:16:44 +02:00
- type-5 descriptors allocate and upload multi-frame bundles through `image_bundle_load_to_vram`
This is why the constructors can seed `obj + 0x10` early and then let later code only vary frame index and state.
## Palette Selection
Palette handling is partly closed and partly still open.
Closed:
- `psx_draw_main_visible_object` reads palette overrides from the original source-record pointer at `obj + 0xa0`
- for types `0x003e..0x00ab`, it uses the high byte of source word `+0x06`
- for types `>= 0x00ac`, it uses the high byte of source word `+0x0c`
Open:
- the full rule for all resource classes and all placement families is not yet completely closed
- the viewer still needs broader CLUT-selection recovery so common cases render with runtime-correct colors without heuristics
So palette work is real, but it is no longer confused with the deeper unresolved art-state bridge.
## How To Reassemble A PSX Map In A Viewer
This is the current best practical recipe.
### Step 1: parse `LSET*.WDL` as a multi-section bundle
Do not treat the first small record stream as the whole map.
Required outputs from the loader stage:
- root/dispatch family payloads
- constructor-placement family payloads
- per-type banks for art/state/variant/simple payloads
- detached extra blob(s)
- optional decoded `DAT_8006769c` substrate/state buffer
- runtime-header block metadata
### Step 2: preserve authored rows as authored rows
Keep the exported record families as reversible scene metadata.
Useful naming already in use:
- `section0_dispatch_roots`
- `section0_constructor_placements`
Do not flatten them into one inferred placement family too early.
### Step 3: reconstruct constructor inputs faithfully
For each candidate placement row, reconstruct at least:
- `type`
- authored selector byte
- authored `x/y/z`
- original source bytes needed for later palette and state analysis
Store the original row pointer or offset in exported metadata where practical.
### Step 4: resolve per-type runtime banks
For each type, export or reconstruct:
- art/template descriptor
- state-script bank
- companion-extents/variant bank
- simple-record payload bank when present
This is already partly supported by the current cache/export path through `stateLayers` and related scene metadata.
### Step 5: seed a viewer-side live object model
Minimum object fields for a faithful viewer simulation:
- resource pointer or resource descriptor reference
- world `x/y/z`
- current script word
- original selector
- current companion extents from `obj + 0x30/+0x34/+0x38`
- type flags
- source-record pointer or reconstructed source bytes
- visible lane classification if known
### Step 6: project with the executable's isometric transform
Use:
- `screen_x = y - x`
- `screen_y = 2*z - (x + y)/2`
Then apply per-frame origin/size offsets and camera subtraction, just as the executable projectors do.
### Step 7: separate stage 1 and stage 2
Do not assume one visible-object list.
Viewer-side representation should support:
- stage-1 main visible list behavior
- stage-2 queued special-visible path
Even if the first viewer implementation collapses them visually, the underlying scene model should keep them distinct.
### Step 8: honor draw-order rules explicitly
The main visible list is sorted, not appended blindly.
A viewer that only sorts by simple `y` or `z` will eventually diverge. The current best approximation should be based on the executable-backed visible-list sort behavior, and exported metadata should preserve enough object/dependency information to improve that later.
### Step 9: use authored palette bytes where proven
The viewer/exporter should keep both:
- default resource palette assumptions
- authored placement override bytes from the preserved source record
That keeps palette work reversible and lets the viewer use stronger executable-backed overrides without hard-wiring them into flattened output.
### Step 10: keep the unresolved state-to-art rule explicit
For unresolved families, do not pretend a flat `type -> frame` table is solved.
Current best practice:
- use verified executable-backed frame maps only where they are genuinely closed
- mark unresolved families as provisional
- preserve the exported state metadata and the `DAT_800758d4` companion-extents metadata so later passes can replace placeholders without redoing the whole loader
- do not treat `DAT_800758d4` as a direct fallback art selector unless a family-specific consumer proves otherwise
## What A Viewer Can Already Do Reliably
Already defensible from evidence:
- load PSX maps from the correct `LSET*.WDL` families
- separate authored record families instead of flattening them
- reconstruct multi-level `z` values for the constructor-placement lane
- use executable-backed projection math
- separate stage-1 and stage-2 world lanes in the scene model
- resolve a first subset of per-type real art from the corrected late `DAT_800758d8` bank
- preserve state banks, companion-extents banks, and decoded runtime blobs as research metadata
## What Still Prevents A Fully Readable Map
The remaining blocker is now narrow and concrete:
- the exact last rule that turns live script/variant state into final visible art for unresolved families
More specifically:
- the constructors, routing, projection, draw passes, and resource submission path are now substantially understood
- `DAT_800758d4` now looks like per-state companion extents used by overlap/contact logic, not the missing final art table
- but for unresolved families the viewer still does not fully reproduce how `DAT_800758cc`, runtime reselection, and family-specific drawable resource/frame presentation interact after the live script changes
That is why the current viewer output is still unreadable as a practical map even though so much of the storage and render machinery is now mapped.
## Recommended Viewer Strategy From Here
Short-term:
- keep the current executable-backed export model
- preserve all state metadata in exported scene JSON
- export the `DAT_800758d4`-backed signed companion extents as explicit runtime-bounds metadata instead of treating them as likely art selectors
- avoid broad fallback art heuristics that overwrite evidence
- use narrow verified family-specific rules only where backed by executable behavior
Current status of that first export step:
- the PSX cache builder now decodes `DAT_800758d4` as a packed per-state signed extents table
- exported `stateLayers` preserve those decoded extents for each type
- each exported scene item and `mapSource` row now carries the resolved `companionExtents` tuple for its chosen live state when available
Medium-term:
- use the recovered companion extents to improve viewer-side inspection and future occlusion/contact overlays
- recover the remaining family-specific state-to-art bridge for unresolved root-dispatch families without assuming `DAT_800758d4` is the art source
- close palette selection more broadly once the art-state path is stable enough
Long-term:
- replace placeholder-heavy families with executable-backed final art selection
- keep scene export reversible so future corrections do not require a fresh reverse-engineering pass over the raw files
## Current Best One-Line Model
The PSX map is stored as a multi-section level bundle plus per-type runtime banks; the executable turns authored rows into live objects, advances and sometimes reseats their state scripts, updates companion extents from `DAT_800758d4`, projects them into one of two world-facing render lanes, and finally draws resource/frame pairs driven by the live script word. The viewer can now reconstruct most of that chain, and the remaining unreadable output is concentrated in the last unresolved live state-to-art bridge for a few still-placeholder-heavy families.