111 KiB
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:
- A PSX level is not stored as one flat placement table.
LSET*.WDLloads 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
- 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.
- 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.
- There are two separate world-facing render lanes:
- stage 1: main visible-object list
- stage 2: queued special-visible list
- The viewer can already reconstruct placement, projection, most resource loading, and much of the draw path from executable evidence.
- The main remaining blocker is the last live state-to-art rule for unresolved families such as
0x0042and0x0049. The map is still unreadable in practical terms because those families still fall back to placeholders. - The current
map 104repeated-wall regression is now clearly over-merged. The cache showstype=0x0042records from both authored section-0 families and multiple rawu5lane/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. - The latest six-track Ghidra pass closes one more structural question:
0x0042is not special at descriptor time. It shares the same generic descriptor cluster as the wider0x003e..0x0050band, so the practical split the viewer still needs is downstream in state, flags, lane routing, and resource kind. - 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 aspsx_object_integrate_motion_and_route_visible(0x8001263c..0x80012c2c), with the local control helper namedpsx_object_update_runtime_input_modes(0x80012c30). The stage-1 ordering lane also now has explicit helper names/contracts forpsx_main_visible_list_swap_entries(0x8002e064),psx_main_visible_order_graph_unlink_pair(0x8002ca74), andpsx_main_visible_order_graph_detach_object(0x8002c89c), reducing ambiguity in rebucket/sort refresh behavior used by the exporter. - The next concrete
map 1040x0042pass now ties the exporter-sideruntimeDiagnosticschema 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 shared0x0042descriptor row, constructors are now proven to seedobj+0x1cby directly copying the authored lane word, the strongest recovered0x0400stage-selection write is still nested-state-side rather than a direct object-local0x0042writer, andDAT_800675f8is now tighter as a level-loaded per-type policy pointer instead of a per-lane discriminator. - A focused object-local route-bit provenance pass over the fixed
map 1040x0042sample pack (item:25/30/31/35/85/86) now tightens theobj+0x1c & 0x0400branch model:psx_object_create_simple_record(0x80024b48) andpsx_object_create_compound_record(0x80025040) remain the strongest concrete object-local writers because they copy authoredu5directly intoobj+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-local0x0400set/clear path; and recovered0x0400writers in this lane are still nested/global (psx_object_state_machine_dispatch_ticknested runtime word at0x8001a078, global policy word inpsx_object_handle_control_pair_0aat0x80022a14). - 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&0x0400routes 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==5image-table else sprite), and CLUT selection is now named throughpsx_clut_table_by_resource_bank(0x800a9f48) pluspsx_clut_override_table_by_palette_token(0x800a9f66).
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.
Final Live Map-104 Cohort Pass (2026-04-12)
Pass objective:
- close the final cohort split question for scene fingerprint
3497e7f641856415on active writableSLUS_002.68 - keep scope fixed to anchor groups already sampled in cache diagnostics:
- root
0x0022: items25/35 - root
0x0030: items30/31 - constructor
0x0030: items85/86 - control
0x0066: item53
- root
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:
0x800249f40x80024eec0x800131a80x80025d680x80041458
Concrete cohort conclusions (what still differs vs still fails to differ):
- 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 root0x0022and0x0030are preserved at spawn instead of being synthesized later. - Still fails to differ (strong): root
0x0030(30/31) and constructor0x0030(85/86) are route-equivalent at creation (obj+0x1cseed), so this pair should not be split by constructor-vs-root family alone. - Still fails to differ (current capture): stage split bit
obj+0x1c & 0x0400remains clear in sampled anchors, so all cohorts continue through stage-1 main-visible rather than diverging by stage lane. - 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==5image-table else sprite) and current anchors lack a proven mixed-kind split. - 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-1040x0042placeholders, with0x0022and0x0030kept 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 seed0x0022 - bucket B:
0x0042+ route seed0x0030
- bucket A:
- do not split
0x0030bucket by root vs constructor origin alone. - do not promote
bit0x0400or 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:
0x800394440x8003917c0x80045ffc0x8002badc0x80040768
Recovered semantics (bundle/CLUT/runtime-bank install):
wdl_resource_bundle_load_by_indexperforms 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.- CLUT install is explicit in
level_palette_header_apply: packed palette header fields are decoded, palette data is expanded, and CLUT blocks are uploaded throughlevel_palette_upload_clutswhile level CLUT mode/state globals are updated. - Per-type state/runtime bank install is explicit in
psx_install_type_state_script_component_extents_banks: it writespsx_type_state_script_bank,psx_type_simple_component_bank, andpsx_type_companion_extents_bankby type row before constructors consume those lanes. - 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. - 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:
0x8004137c0x800414580x800411440x80040d440x80044bdc0x80044e9c
Recovered semantics (draw submission and palette token):
- 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. - 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
- if
- Main-visible injects authored palette token into submit flags before dispatch:
- base flags include
obj_flags & 0x0002 - for
type 0x003e..0x00ab: token high byte fromsource+0x06 - for
type >= 0x00ac: token high byte fromsource+0x0c
- base flags include
- Special-visible does not inject authored palette token high byte; it passes only orientation/flip bits (
obj_flags & 0x0002) into submitters. - 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)]
- default:
- Frame token bridge is explicit in projection and submit:
obj+0x94is used bypsx_project_object_main_visibleframe-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.
- lane
- For unresolved families (
0x0042,0x0049,0x0055..0x0063), include both fields in runtime diagnostics and donor-key matching:- bound resource kind (
spritevsimage-tablepath) - latched frame token (
obj+0x94)
- bound resource kind (
- 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+0x9epath) from final frame latch (obj+0x94path)
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:
0x800185780x8001bca00x8001e6e80x800260e80x80025d68
Recovered selector/transition semantics (exact current read):
- Type-
0x0042pre-latch reseat is explicitly gated before selector edits:psx_type42_transition_selector_tickfirst requirespsx_object_is_within_view_margin(obj, 0x14)and object lane bitobj+0x1c & 0x0020before its turn/heading reseat path proceeds. - 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 fromDAT_80063b4c[(type-0x1e)*0x0f + transition_code]. - 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
- mode/policy row @
- 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 in0x80018578callpsx_object_select_state_scriptwith wrapped heading buckets (& 7) before latch. psx_object_select_state_scriptis confirmed as selector install only (obj+0x9e,obj+0x8c/0x90cursor); final visible token still latches later inpsx_object_advance_state_scriptvia write toobj+0x94.
Strongest evidence for selector-to-visible-art linkage:
- The same live chain now stays consistent across all inspected callsites: pre-latch selector reseat (
0x80018578and0x8001bca0) -> selector install (0x800260e8) -> frame/state latch (0x80025d68, write toobj+0x94) -> projection/draw frame queries. This is the clearest current evidence that selector3/4effects 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+0x9epath)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
0x800758d8and0x800758c8 - 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:
0x80045ffc0x800249f40x80024eec0x80038f180x80041458
Strongest install->draw evidence recovered:
psx_install_type_art_active_header_and_built_resourcefirst writes the incoming header pointer intopsx_type_art_active_header_bank[type](0x800758d8lane), then performs kind dispatch (4single-image bind,5bundle build/upload) and commits the resolved runtime resource pointer intopsx_type_art_built_resource_bank[type](0x800758c8lane).- 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.
- Both constructors (
simpleandcompound) bindobj+0x10from the per-type lane: if active entry kind is5, they reusepsx_type_art_built_resource_bank[type]; otherwise they invokepsx_create_image_resource_from_descriptorand refresh cache state. psx_stream_install_type_runtime_banksseeds/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.psx_draw_main_visible_objectuses ctor-boundobj+0x10plus live frame tokenobj+0x94; submitter choice is resource-kind based (kind==5->psx_image_table_submit_frame, elsepsx_sprite_resource_submit_frame).
Conservative semantics confirmed in this pass:
0x800758d8is the active per-type art lane at install/constructor handoff time.0x800758c8is 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) andobj+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
0x0042bands, do not treat type-level art as a statictype -> bundletable. The executable path istype 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_bank0x800a9f66->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 contract0x8002c89c: full order-graph detach contract0x80041378: fixed world draw pass sequence (stage-1, stage-2, then HUD)0x80041458: main-visible submitter and palette-token override rule0x80041144: stage-2 submitter rule parity, no authored palette-token override0x80044bdc: sprite submit upload/refresh and CLUT override behavior0x80044e9c: image-table submit metadata-only frame resolve and CLUT override behavior
Strongest evidence recovered:
- Stage routing split in
0x800131a8is explicit and branch-local:type==4 || (obj+0x1c & 0x0400)callspsx_project_object_special_visible_queue; the fallthrough path callspsx_project_object_main_visible. - World pass submission order in
0x80041378is explicit and fixed: iterate sorted main-visible slice -> draw special-visible queue -> draw HUD/overlay. - Stage-1 ordering is not a plain z-sort:
0x8002d778repeatedly resolves dependency counts and may callpsx_main_visible_order_graph_unlink_pair; graph maintenance is handled by0x8002ca74and bulk detach by0x8002c89c. - Submitter choice is lane-independent and strictly resource-kind based at both call sites (
0x80041458,0x80041144):kind==5routes topsx_image_table_submit_frame; elsepsx_sprite_resource_submit_frame. - Palette handling differs by lane: main-visible computes an authored high-byte token from source fields (
+0x06for0x003e..0x00ab,+0x0cfor>=0x00ac) and ORs it into submit flags, while special-visible currently passes only orientation-flip bit (no authored token injection). - Both submitters converge on identical CLUT override semantics: default from
psx_clut_table_by_resource_bank, optional override throughpsx_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 (
5image-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:
- 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.
- Cross-map donor reuse is now fenced by recovered active-header
payloadKindfromDAT_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/0x800675ecand per-type policy table0x800675f8can directly explain unresolved visible-art family splits onmap 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 at0x8001353c) - edited (comment)psx_draw_main_visible_object(0x80041458, policy read at0x80041604) - 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_block0x8002e484:FUN_8002e484->psx_marker_channel_get_mode2_meter_value0x8002e498:FUN_8002e498->psx_marker_channel_try_spend_mode2_meter0x8002e3e8:FUN_8002e3e8->psx_marker_channel_add_mode2_meter_and_queue_event0x800308ac:FUN_800308ac->psx_marker_channel_refresh_mode2_active_slot0x800304c4:FUN_800304c4->psx_marker_channel_cycle_active_slot_by_direction
Recovered island/policy interaction semantics (strongest current read):
0x80063e68and0x80063e54act as a reciprocal control-gating pair in post-load/passcode transition flow: selected map id maps to slot via0x80063e68, then is validated by reverse map lookup via0x80063e54before mode-action side effects proceed.0x800675ecis 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.0x800675f8policy words are consumed in routing/order/draw as behavior bits by type:- route-side read (
0x80013550) uses0x1000for nearby-interaction publication behavior after lane route decision - draw-side read (
0x8004161c) uses0x2000for a render-state branch after submitter/lane are already chosen - order-graph compare (
0x8002bf2c) uses0x0600class behavior during pair ordering
- route-side read (
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 & 0x0400at0x80013518, not a direct read of0x800675f8or the0x63e54/0x63e68/0x675ecisland. - 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+0x94latched frame token, and route bitobj+0x1c & 0x0400 - island/policy words should be recorded as secondary modifiers (
orderingClass,drawPolicyBits,publishPolicyBits) to explain same-resource visual divergence
- primary split keys remain bound resource kind,
- 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 104type0x0042 - 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)psx_cache_type_art_descriptor_and_resource(0x80045ffc)
- section-0 authored dispatch:
psx_dispatch_section0_dispatch_roots(0x800256b0)psx_dispatch_section0_constructor_placements(0x800258cc)- type-
0x0042descriptor 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_800676d8psx_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_bufferFUN_8003aba8->psx_lzss_pack_level_bufferDAT_8006b5d8->psx_level_state_compressed_blobDAT_8006763c->psx_level_heap_cursor
- comments:
- at
0x80039af0: compressed level-state blob loads intopsx_level_state_compressed_blob, then unpacks intopsx_level_decompressed_state_bufferbefore root/constructor dispatch - at
0x800249f4: constructor bind note forpsx_type_art_active_header_bank[type]andpsx_type_art_built_resource_bank[type]
- at
Recovered ownership chain (strongest current read):
wdl_resource_bundle_load_by_indexreads 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 asDAT_80067938,DAT_80067838,psx_type_policy_table_ptr,DAT_80067840,DAT_800676d8).- The same loader reads compressed state into
psx_level_state_compressed_bloband unpacks throughpsx_lzss_unpack_into_level_bufferintopsx_level_decompressed_state_buffer(DAT_8006769c), then proceeds with root/constructor section dispatch setup. - Type runtime banks are installed from subordinate bundle sections:
psx_load_type_state_banksseedspsx_type_state_script_bank,psx_type_simple_component_bank, andpsx_type_companion_extents_bank; descriptor/resource install paths seedpsx_type_art_active_header_bankandpsx_type_art_built_resource_bank. - Section-0 authored records feed object construction via descriptor-table callback rows. For
0x0042, both root and constructor families resolve through0x80063220 -> 0x800626f8and enter the same generic create/update/release family. - 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]andpsx_type_art_built_resource_bank[type], while draw/update logic also consumespsx_type_policy_table_ptr[type]policy bits.
Storage nodes still unresolved (blocking full map 104 0x0042 closure):
- exact schemas and semantics for
DAT_80067938,DAT_80067838,DAT_80067840, andDAT_800676d8(currently installed by offset in the section pack but weakly typed downstream) - map-104-specific ownership/meaning of
psx_level_decompressed_state_bufferpayload slices after inflate (which words materially feed type-0x0042object family divergence) - 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 shared0x0042descriptor row - final control interaction between section-installed policy (
psx_type_policy_table_ptr), constructor-seededobj+0x1cflags, 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
0x0042and control type0x0066 - 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 at0x800398f0
- key readers sampled directly:
psx_object_integrate_motion_and_route_visible(0x8001353c) ->& 0x1000psx_draw_main_visible_object(0x80041604) ->& 0x2000psx_main_visible_order_compare_pair_for_graph(0x8002bf0c,0x8002c174,0x8002bf5c) ->& 0x0600psx_object_state_machine_dispatch_tick(0x8001a280,0x8001aa44) ->& 0x0800psx_object_update_nearby_interactions(0x8002957c,0x80029970, etc.) ->& 0x0100,& 0x4000,& 0x0008psx_object_register_contact_pair/ related overlap helpers (0x80028488family) ->& 0x0008psx_find_nearest_los_target_with_typeflag10_or20(0x800149cc,0x80014b14) ->& 0x0010,& 0x0020psx_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: markspsx_type_policy_table_ptrinstall from level section pack (DAT_80067838 + local_b4)0x8002bf0c: marks0x0600class compare role in stage-1 ordering0x80041604: marks0x2000draw-time semitrans gate0x800140c8: marks0x0080policy gate before proximity trace selection
Best evidence on provenance and storage ownership:
psx_type_policy_table_ptrhas exactly one recovered writer in the executable (0x800398f0) and is rebuilt duringwdl_resource_bundle_load_by_index.- 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). - This establishes ownership as level-loaded runtime storage (LSET bundle section data), not immutable executable ROM data.
- Static executable-only recovery of concrete policy rows for type
0x0042/0x0066remains blocked because the table contents are not authored in-place in the code image. - Runtime-adjacent cache evidence is still incomplete for these rows: current map-104 sample rows (
25/30/31/35/85/86for0x0042,53for0x0066) still exportruntimeDiagnostic.typePolicy.sampled=falseandword=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
0x004264x64vs64x40split 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, rootu5=0x0030, constructoru5=0x0030 - fixed sample pack:
item:25/30/31/35/85/86
Concise provenance summary (inspected writer/preserve/transform chains):
-
Object-local writers (direct
obj+0x1cstores):psx_object_create_simple_record(0x80024b48) — copies authoredu5intoobj+0x1cat 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
0x0400set/clear):psx_type42_transition_selector_tickpsx_object_select_state_from_transition_tablepsx_object_advance_state_scriptpsx_apply_deferred_control_to_live_objectspsx_object_handle_control_pair_0a
-
Strongest recovered non-object-local
0x0400writers (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
0x0400controls in same neighborhood:0x8001a078,0x80022a14
Recovered chain for object-local 0x0400 in this sample lane:
- Section-0 authored row feeds constructor.
- Constructor copies authored
u5directly intoobj+0x1c(psx_object_create_simple_record/psx_object_create_compound_record). - Later named mutators in the
0x0042path rewrite low control bits (0x0001/0x0002/0x0004/0x0020/0x0040/0x0080/0x0100/0x0200) but no inspected object-local write site introduces a newobj+0x1c |= 0x0400orobj+0x1c &= ~0x0400transform. - Route split still consumes
obj+0x1c & 0x0400atpsx_object_integrate_motion_and_route_visible.
Current strongest read stays unchanged but tighter:
- strongest object-local writer for
0x0400presence/absence is still authoredu5copied at object creation - strongest
0x0400writer recovered in active runtime logic remains nested/global state, not object-localobj+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/86because static writer recovery still does not pin a direct object-local runtime transformer for bit0x0400
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_tablebase)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_recordpsx_object_create_compound_recordpsx_dispatch_section0_dispatch_rootspsx_dispatch_section0_constructor_placementspsx_run_live_object_type_updatespsx_object_select_state_from_transition_tablepsx_object_integrate_motion_and_route_visiblepsx_draw_main_visible_objectpsx_cache_type_art_descriptor_and_resourcepsx_level_post_load_runtime_resetpsx_section0_dispatch_root_seed_marker_channel_table
Applied live symbol updates (small, evidence-backed):
0x8002f190->psx_marker_channel_dispatch_mode_action0x8002f250->psx_marker_channel_runtime_state_clear0x80031840->psx_marker_channel_mode_is_enabled0x8003185c->psx_marker_channel_get_mode_step_value0x80031878->psx_marker_channel_runtime_state_snapshot0x80031a3c->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 to0x90-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 withDAT_80063e68as profile/eligibility control.
Concrete table-role clarifications from this pass:
- Type
0x0042still resolves through generic descriptor table fetch atpsx_type_descriptor_table[0x0042](no unique descriptor fork). - The transition selector row table at
0x80063b4cremains narrow and state-chooser specific (only direct reads inpsx_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/0x800675eccluster 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-0x0042divergence 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_ptrDAT_800676d8->level_clut_table_ptrDAT_80067840->psx_control_opcode_stream_tableDAT_80063e54->psx_level_selector_table_80063e54DAT_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_ptris 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 == 0x0042comparison 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
0x0042shape/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_80063e68is used by section-0 marker/channel seeding and related dispatch controlpsx_level_selector_table_80063e54is 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_bufferis still the unpack destination and immediate source for runtime-bank installationpsx_load_type_state_banksremains the installer forDAT_800758cc/d0/d4psx_stream_install_type_runtime_banksremains 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
0x0042hiding behind one unnamed unique descriptor fork” - but “which installed rows and control tables still explain the concrete map-104
0x0042family split after load”
Highest-value unresolved tables/structures after this pass:
DAT_800675ecstruct layout (0x90bytes): fields at+0x34bit-flag rows,+0x6cstep-value rows, and+0x84..+0x8fstate words still need a named datatype.DAT_80063e68andDAT_80063e54exact schema (entry count/semantics) and whether they directly separate map-1040x0042families or only gate marker/control channels.- Type-
0x0042runtime-bank interpretation at constructor and post-state-advance points: exact relationship betweenDAT_800758d8[type]active header,DAT_800758c8[type]built resource, and effective submitted frame group for sample items25/30/31/35/85/86. - Per-type policy word capture for
0x0042in 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_loop0x80031f0c->psx_lset_world_frame_wrapper0x80031f6c->psx_lset_session_teardown0x800350a8->psx_render_mode_dispatch0x80039ef4->psx_level_post_load_runtime_reset0x80044104->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:
psx_lset_session_loopdrives map-index loads and frame loop control.lset_level_bundle_loadloads runtime blobs and stream payload lanes.- Per-type bank installs (
psx_load_type_state_banksand descriptor-bank writes) seed runtime object resources. - Runtime-header and post-load reset apply mission/level state before steady-state updates.
psx_lset_world_frame_wrapperdispatches render mode, world visible draw, andpsx_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_record0x80031044->psx_section0_dispatch_root_find_marker_record_by_channel0x800460fc->psx_upload_spec_wdl_image_pair_to_vram0x800463bc->psx_restore_display_draw_env_after_spec_uploadDAT_800675f8->psx_type_policy_table_ptrDAT_80063a00->psx_type_transition_mode_policy_rowsDAT_80063b4c->psx_type_transition_selector_rows
Family bridge is now explicit
psx_dispatch_section0_dispatch_rootsis the root-family entry.psx_dispatch_section0_constructor_placementsis 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 sharedpsx_object_create_compound_record/psx_object_advance_state_scriptpath.
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_tickstill remains the stronger explanation for live selector3/4cases 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 initial0x0022versus0x0030split after the fact.
For the current map 104 sample pack this means:
- lane
0x0022starts as authored{ 0x0020, 0x0002 } - lane
0x0030starts 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
0x0400write remains nested-state-side in the wider runtime state machine. 0x0042continues to read and rewrite nested runtime-state words in the same neighborhood.
So the current best read stays split:
- object-local
obj+0x1cis real authored route/orientation state - nested runtime state still carries the strongest recovered
0x0400stage-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 << 1and test the resulting halfword. - The currently strongest known masks remain:
0x1000: nearby-interaction publication suppression0x0600: stage-1 ordering class0x2000: 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_overlap0x80018414->psx_transition_selector_probe_marker_overlap0x800261f4->psx_snapshot_active_object_runtime_rows0x80025b88->psx_release_all_active_objects_and_reset_type_runtime_banks0x8002f518->psx_section0_dispatch_root_seed_marker_channel_table0x80030ee0->psx_section0_dispatch_root_lookup_marker_record_by_kind_channel0x80030f60->psx_section0_dispatch_root_lookup_marker_slot_by_kind_channel0x80030fcc->psx_section0_dispatch_root_get_marker_slot_triplet_by_index0x800311c4->psx_section0_dispatch_root_apply_packed_channel_actions0x80031454->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 104sample pack, lane0x0022versus0x0030is 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_scriptinstalls selector identity intoobj+0x9e, plus the script cursor and intermediate token state.psx_object_advance_state_scriptlater latches the live frame/state token intoobj+0x94.- Both
psx_project_object_main_visibleandpsx_project_object_special_visible_queuethen consumeobj+0x94for 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_visibletestsobj+0x1c & 0x0400at the decisive branch.- For
0x0042, the type-4special case is irrelevant, so object-local0x0400remains the practical stage-2 discriminator. - The strongest recovered
0x0400write is still nested-state-side rather than a direct type-0x0042object-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+0x1cactually carries0x0400at 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:
0x800260e8psx_object_select_state_script: installs selector toobj+0x9e0x80025d68psx_object_advance_state_script: latches current frame/state intoobj+0x940x80040d44psx_project_object_main_visible: readsobj+0x94, then queries frame origin/width/height fromobj+0x100x80040f78psx_project_object_special_visible_queue: sameobj+0x94frame-geometry query path0x8004513cpsx_resource_frame_origin_x,0x800451d0psx_resource_frame_origin_y0x80045014psx_resource_frame_width,0x800450a8psx_resource_frame_height
Decisive evidence from decompile:
- The projector functions read frame token from
obj+0x94before any frame-geometry call. - Width/height/origin helpers branch only on resource kind (
4sprite-header frame records vs5image-table frame records) and use the caller-provided frame index. - No later projector/draw step rewrites width/height into a
64x40vs64x64family 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 4vskind 5access path), but for same-type0x0042objects this is type-level and upstream of per-object frame choice.
Minimum runtime fields to preserve in exporter/viewer for faithful reproduction:
obj+0x10bound resource identity, including resourcekind(4vs5)obj+0x94live latched frame/state token (the geometry key)obj+0x9eauthored selector seed (kept separate fromobj+0x94)obj+0x1croute/orientation flags (0x0002flip behavior and0x0400stage route)
Concrete sample-pack sanity check from current scene cache (map 104, fingerprint 3497e7f641856415):
- item
25and35currently render64x40with route seed0x0022 - item
30,31,85,86currently render64x64with route seed0x0030
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: items25/35 - root
0x0030: items30/31 - constructor
0x0030: items85/86 - control
0x0066: item53
- root
Functions inspected this pass (create/update/draw + ordering):
psx_object_create_simple_record(0x80024b48) - inspected, comment updatedpsx_object_create_compound_record(0x80025040) - inspected, comment updatedpsx_object_integrate_motion_and_route_visible(0x800131a8) - inspected, comment updatedpsx_object_advance_state_script(0x80025d68) - inspected, no edit this passpsx_draw_main_visible_object(0x80041458) - inspected, no edit this passpsx_draw_special_visible_queue(0x80041144) - inspected, no edit this passpsx_main_visible_order_compare_pair_for_graph(0x8002bf0c) - inspected, comment updated
Live artifacts changed in this pass:
- decompiler comments added/updated:
0x80024b480x800250400x800131a80x8002bf0c
Concrete cohort findings from scene-cache fields and executable behavior:
- 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
0x0066sample (53) ->initialWord=0x0020
- root
- visible frame geometry differs in exported candidates:
25/35->64x4030/31/85/86->64x6453->64x32
- object-local route seed differs exactly as authored:
- Differences that still fail to differentiate the anchor cohorts:
- stage-route discriminator
bit0x0400remains 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 type0x0042and control type0x0066anchors. - per-type art-header kind remains unsampled (
resourceKind.sampled=false), so the kind-4 versus kind-5 split is still unresolved at item granularity.
- stage-route discriminator
Strongest executable evidence for the next exporter rule:
- constructors directly preserve authored route/lane word into
obj+0x1c(0x80024b48,0x80025040), so0x0022versus0x0030is 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) in0x8002bf0care ordering-precedence modifiers, not resource-binding keys.
Practical next exporter rule (highest confidence):
- Keep map-104
0x0042cohort partitioning keyed first by authoredrecord_u5/obj+0x1cseed families (0x0022vs0x0030) and keep control0x0066separate. - Do not use
typePolicyor stage-route0x0400as 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.initialWordmust match exactly (0x0022group separate from0x0030;0x0066control separate from0x0042).
- Treat
typePolicy.wordandresourceKind.activeHeaderKindCandidateas 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.
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
.cachescene/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_indexpsx_object_create_simple_recordpsx_object_create_compound_recordpsx_object_select_state_scriptpsx_object_advance_state_scriptpsx_object_lookup_variant_entrypsx_object_integrate_motion_and_route_visiblepsx_object_update_runtime_input_modespsx_project_object_main_visiblepsx_project_object_special_visible_queuepsx_draw_world_visible_passespsx_draw_main_visible_objectpsx_draw_special_visible_queuepsx_main_visible_list_swap_entriespsx_main_visible_order_graph_unlink_pairpsx_main_visible_order_graph_detach_objectpsx_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)
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
psx_cache_type_art_descriptor_and_resource(0x80045ffc) stores the per-type descriptor atDAT_800758d8[type]and materialized drawable resource atDAT_800758c8[type].- 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 callimage_bundle_load_to_vram(0x80044614)
psx_create_image_resource_from_descriptor(0x80044434) uses the same discriminator:0x8004445c:kind == 4branch0x80044468:kind == 5branch0x8004447c: callpsx_resource_bind_single_image_vram_slot0x800444ac: callimage_bundle_load_to_vram
Draw-time submitter selection
psx_draw_main_visible_object:0x800415a8: compareresource->kindagainst50x800415c0:kind == 5->psx_image_table_submit_frame(0x80044e9c)0x800415e0: otherwise ->psx_sprite_resource_submit_frame(0x80044bdc)
psx_draw_special_visible_queue:0x800412c8: compareresource->kindagainst50x800412dc:kind == 5->psx_image_table_submit_frame0x800412f8: otherwise ->psx_sprite_resource_submit_frame
psx_draw_hud_overlay_passhas the same split for slot-managed overlays:0x80041b5c: image-table submit path0x80041b84: 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_objectandpsx_draw_special_visible_queuebranch directly onresource->kind == 5.psx_draw_hud_overlay_passcan 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_overlayis 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 == 5as table-backed UV/frame metadata (psx_image_table_submit_frame) andresource kind != 5as 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.
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_indexopensSPEC_A.WDLand the selectedLSET*.WDL- it reads a
0x38header 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 streampsx_section0_dispatch_root_records(DAT_80067720): secondary0x18-stride authored record familypsx_section0_constructor_placement_records(DAT_800678f0):0x0c-stride constructor-placement familypsx_type_art_template_bank(DAT_800758d8): per-type art/template descriptor bankpsx_type_simple_component_bank(DAT_800758d0): per-type simple-record payload bankpsx_type_state_script_bank(DAT_800758cc): per-type state-script bankpsx_type_companion_extents_bank(DAT_800758d4): per-type variant/companion bankDAT_800675f8: per-type flags tablepsx_level_detached_blob(DAT_8006767c): additional detached level blobDAT_8006b5d8 -> psx_level_decompressed_state_buffer(DAT_8006769c): optional decompressed0x3e00runtime/state substratepsx_level_runtime_header_block(DAT_80067794): separate0x50level runtime-header block
The loader/install ownership is now tighter too:
psx_load_type_state_banksinstalls theDAT_800758cc/d0/d4banks only.psx_stream_install_type_runtime_banksis 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 atDAT_800758c8.psx_snapshot_level_runtime_header_blockandpsx_apply_level_runtime_header_blockmake theDAT_80067794lane read as save or transition state, not as the missing per-type art-binding source for unresolved0x0042families.
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 theDAT_80067720/ root-dispatch familysection0_constructor_placements: closest to theDAT_800678f0constructor-input family
These runtime anchors are now named the same way in the live Ghidra database:
section0_dispatch_rootsaligns withpsx_section0_dispatch_root_recordssection0_constructor_placementsaligns withpsx_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
3. The descriptor-table cluster for 0x003e..0x0050 is shared
The newest Ghidra pass tightens the per-type descriptor story substantially.
psx_type_descriptor_tableat0x80063118is a pointer table.- Every currently sampled type in the
0x003e..0x0050band points to the same descriptor object at0x800626f8. - Type
0x0042is now pinned exactly too:psx_type_descriptor_table[0x0042]at0x80063220points to0x800626f8. - That shared descriptor currently resolves to a small callback cluster that includes:
psx_spawn_compound_record_advance_state_oncepsx_object_refresh_main_visible_and_cleanuppsx_object_release_to_free_listpsx_spawn_simple_record_set_active_flagpsx_object_advance_state_and_queue_special_visiblepsx_object_create_simple_recordpsx_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 callbackdescriptor + 0x04: per-object update callback copied into the live objectdescriptor + 0x08: release callbackdescriptor + 0x0c: descriptor flags
The newest pass also tightens the strongest known constructor-placement route for 0x0042 itself:
psx_spawn_compound_record_advance_state_oncepsx_object_create_compound_recordpsx_object_advance_state_scriptpsx_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
The per-type art bank in DAT_800758d8 is not taken from the earlier small-section heuristic.
Current best read:
- the useful late
DAT_800758d8candidate sits in a late large section - it decodes only when that section is treated as a bank with an embedded
+0x38parse start - on retail map
9, that correction lifts resolved bundle-mapped items from0to111
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.
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]andpsx_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, whilepsx_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_bankDAT_800758c8=psx_type_art_built_resource_bank
Constructor Record Layouts
Two constructor families are now strong enough to describe directly.
Compound-record constructor
psx_object_create_compound_record reads:
typefromrecord + 0x00xfromu16at+0x02yfromu16at+0x04zfrom byte+0x06- initial state selector from byte
+0x08 - flags from
+0x0a
Simple-record constructor
psx_object_create_simple_record reads:
typefromrecord + 0x04xfromu16at+0x08yfromu16at+0x0azfrom byte+0x0c- initial state selector from byte
+0x0e
Common constructor outputs
Both constructors:
- write authored coordinates into object fields
+0x3c/+0x40/+0x44as16.16fixed-point - preserve the original authored source-record pointer at
obj + 0xa0 - copy the authored lane/flags word into
obj + 0x1c - 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_scriptto 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.
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
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 bankpsx_type_simple_component_bank(DAT_800758d0): simple-record local payload bankpsx_type_state_script_bank(DAT_800758cc): state-script bankpsx_type_companion_extents_bank(DAT_800758d4): variant/companion bank
Important object fields
obj + 0x10: current drawable resource pointerobj + 0x1c: live route/flags word copied from the authored row and later consumed by visible-routing helpersobj + 0x20..0x2e: projected on-screen rectangleobj + 0x3c/+0x40/+0x44: fixed-point worldx/y/zobj + 0x54/+0x58/+0x5c: next/target world position cluster used by motion/integration helpersobj + 0x60/+0x64/+0x68: motion vector used by heading/state reselection helpersobj + 0x78/+0x7c: intermediate projected screen anchor in fixed-pointobj + 0x84: current variant bank pointerobj + 0x88: current state-script table pointerobj + 0x8c/+0x90: active script base and current script cursorobj + 0x94: current script word, which is already the live frame/state index used by later draw helpersobj + 0x9e: original authored selector stored bypsx_object_select_state_scriptobj + 0xa0: original authored source-record pointer
The crucial distinction is:
obj + 0x9eis the authored input selectorobj + 0x94is 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 + 0x94from the current script word - reruns
psx_object_lookup_variant_entry
Verified sentinel/control behavior now includes:
0xfffe:psx_script_dispatch_audio_event, a non-visible audio/sequence side-effect dispatch0xfffd: direct in-family jump0xfffc: immediate switch to subsidiary script-table entry0xfffb: scan-forward variant that consumes the next in-band0xfffdselector 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_3duses those fields as the object's overlap extents againstobj + 0x54/+0x58/+0x5cpsx_object_update_contact_block_flagsuses the same extents while setting directional block/contact bitspsx_object_reselect_state_from_target_vectorandpsx_type4_reselect_motion_stateuse target-object+0x30/+0x34/+0x38as 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_vectorpsx_object_quantize_motion_heading16psx_quantize_vector_heading16psx_object_select_state_from_transition_tablepsx_type42_transition_selector_tick
The live reselection path is now slightly tighter than before:
psx_heading16_lookup_unit_vectoris the table-backed heading-to-vector helper used bypsx_object_reselect_state_from_target_vectorwhen a target-side heading token is available
Verified interaction/reselection cluster:
psx_type4_update_delayed_interactionpsx_type4_reselect_motion_statepsx_object_update_nearby_interactionspsx_object_test_overlap_3dpsx_object_update_contact_block_flagspsx_object_register_contact_pair
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_pair0x800281d4=psx_object_test_strict_nonoverlap_flag8_subject0x80028700=psx_object_adjust_param9c_by_view_side0x800287bc=psx_object_update_param9c_from_contact_target0x80028eb4=psx_object_apply_contact_push_bias0x8002923c=psx_object_spawn_type11_contact_proxy0x8001ae9c=psx_object_update_interaction_transition0x8001bca0=psx_object_select_state_from_transition_table
Current practical read for exporter-facing behavior:
+0x30/+0x34/+0x38remains the shared runtime companion-extents lane used by overlap/reselection checks.+0x9cis actively rewritten after contact/reselection and camera-side scaling, so it should not be treated as spawn-static metadata.- contact-triggered type-
0x11proxy 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_tableuses the per-type table atDAT_80063b4cplus heading-bucket logic to choose selectors beforepsx_object_select_state_scriptreseats the active script. psx_object_update_interaction_transitionshows one concrete forced-selector path: spawned helper objects are pushed to selector3unless they are already in selector1or3.- type
0x0042now has a dedicated transition/update helper too:psx_type42_transition_selector_tickcomputes and dispatches low turning selectors before the+0x94-style runtime latch copy. - for type
0x0042, theDAT_80063b4crow itself mostly yields higher script selectors; the low selectors3/4are 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_codeis a transition-effect constructor helper used in the same0x0042transition lane and confirms that selector install writesobj + 0x9ewhile the later state advance still owns theobj + 0x94latch.
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.
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_tickusespsx_object_is_within_view_marginas 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 callpsx_object_select_state_scriptbefore 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_placementswithu5=0x0020section0_dispatch_rootswith a largeu5=0x0030band- smaller
section0_dispatch_rootsoutliers withu5=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.
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.
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 rawu5, with decoded0x0002/0x0020/0x0200/0x0400bits and a route-outcome candidateselector: raw selector seed plus an explicit note when the type-0x0042pre-latch turn path may diverge from the later latchlatchedState: current exporter-side state/frame candidate used for scene art selectionnestedRuntimeState: explicit placeholder slots for the live nested runtime words that still need Ghidra-side samplingresourceKind: per-type active-header/built-resource hints derived from the current art-bank decodetypePolicy: explicit placeholder slot for the liveDAT_800675f8word
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.
Per-Frame World And Render Pipeline
1. Outer lifecycle
psx_level_session_loop: outer level-session loopwdl_resource_bundle_load_by_index: level load and runtime-bank setuppsx_world_frame_tick: normal per-frame world looppsx_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 theDAT_80067720family plus nearby fixed-size entriespsx_dispatch_section0_constructor_placements: dispatches theDAT_800678f0constructor-placement familypsx_authored_record_in_view_bounds: shared screen-space cull gate for those authored record families before handler dispatch
These are the closest executable matches for the viewer's exported authored record families.
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.
3. Live-object update lane
psx_run_live_object_type_updates: per-type live-object update callback passpsx_run_live_object_behavior_callbacks: later per-object behavior callback passpsx_object_integrate_motion_and_route_visible: integrates motion, updates visibility flags, advances script state, and routes the object to the appropriate render lane
That lane now has two more verified helper clusters that matter for map reconstruction:
psx_run_object_behavior_program_tickandpsx_object_behavior_opcode_dispatchservice a small per-object timed opcode stream before or alongside the main motion/update pathpsx_world_point_in_view_boundsis the shared world-space cull helper used both bypsx_object_integrate_motion_and_route_visibleandpsx_run_live_object_type_updatespsx_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, andpsx_apply_deferred_control_to_live_objectsform 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_listpsx_nearby_interaction_list_addpsx_nearby_interaction_list_removepsx_update_motion_and_nearby_interactions
Current best read:
psx_object_integrate_motion_and_route_visibleandpsx_object_refresh_main_visible_and_cleanupenqueue eligible objects into a nearby-interaction active set after projection/state refreshpsx_update_motion_and_nearby_interactionsconsumes that active set before the next frame, runningpsx_type4_update_delayed_interactionfor type4objects andpsx_object_update_nearby_interactionsfor the broader collision/contact lanepsx_control_move_player_to_pointandpsx_control_move_object_to_pointclose control opcode case1as a move-to-point instruction rather than a render-side helper,psx_control_wait_tickscloses case3as a timed gate onDAT_80078a28,psx_control_configure_fixed_camera_anchorcloses cases4/5as the fixed-camera-anchor configurator, andpsx_control_set_facing_directioncloses case9as an explicit heading override- control opcode case
8is still not named at the wrapper level, but its direct callee is now grounded:psx_spawn_object_compound_effect_variant3creates a type-2compound 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
4. Stage 1 versus stage 2 is a real runtime split
The split point is now explicit inside psx_object_integrate_motion_and_route_visible, not just inferable from later draw helpers.
obj + 0x1cbit0x0020keeps the object in the broad world-visible route class.obj + 0x1cbit0x0400selects the stage-2 path: set routes topsx_project_object_special_visible_queue, clear falls through topsx_project_object_main_visible.obj + 0x1cbit0x0002still reads as orientation/extents behavior only; it does not switch lanes by itself.DAT_800675f8[type]bit0x1000gates the nearby-interaction publish call after projection, and bits0x0600feed the stage-1 order-graph comparator.
The writer side is now tighter too:
- constructors seed
obj + 0x1cdirectly from the authoredu5word psx_object_select_state_from_transition_tablecan set or clear bit0x0002- the strongest recovered stage-2 consumer remains the route test itself; no equally strong local writer for bit
0x0400has been confirmed yet in the named helper set, so current best read is still that0x0400is 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_tickcontains a confirmed write of bit0x0400, but to the nested runtime state word, not directly to the object-localobj + 0x1chalfword.psx_object_handle_control_pair_0acan set global policy bits inDAT_80078a88, clear the nested runtime+0x1cdword, and set object-local bit0x0200, but this pass still did not recover a direct object-localobj + 0x1c |= 0x0400write.
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.
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 countpsx_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.
The type-flag lane is now more concrete too:
DAT_800675f8[type] & 0x1000: suppresses nearby-interaction publication after the stage-2 route pointDAT_800675f8[type] & 0x0600: stage-1 ordering priority class used by the order graph and slice sorterDAT_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_passesends withpsx_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_loopcallspsx_hud_overlay_init_resources(0x800388a8) during level-session bring-up.psx_hud_overlay_init_resourcespreloads fixed HUD resources and descriptor defaults used by the later overlay pass.psx_draw_hud_overlay_passconsumes the slot table and callspsx_overlay_slot_step_color_fade(0x80038114) per active slot.- overlay/menu controllers allocate and retire those slots through
psx_overlay_slot_create(0x80035cc0) andpsx_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.
5. Small wrapper helpers prove the split is intentional
Recovered wrappers:
psx_spawn_compound_record_advance_state_oncepsx_spawn_simple_record_set_active_flagpsx_object_refresh_main_visible_and_cleanuppsx_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:xas16.16obj + 0x40:yas16.16obj + 0x44:zas16.16
Projection
psx_project_object_main_visible and psx_project_object_special_visible_queue use:
screen_anchor_x = y - xscreen_anchor_y = 2*z - (x + y)/2
They write the fixed-point intermediate anchor to:
obj + 0x78obj + 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:
- asks
psx_main_visible_list_get_sorted_slicefor the current sorted stage-1 slice - iterates it through
psx_draw_main_visible_object - then draws stage 2 through
psx_draw_special_visible_queue - then executes the HUD/overlay pass
2. Stage-1 visible-list management
Named helpers:
psx_main_visible_list_addpsx_main_visible_list_removepsx_main_visible_list_rebucket_objectpsx_main_visible_list_refresh_from_live_chainpsx_main_visible_list_sort_rangepsx_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
The final submitter split is now explicit:
- both
psx_draw_main_visible_objectandpsx_draw_special_visible_queuechoose the submitter from the bound resource header atobj + 0x10, not from type id alone - if
resource->kind == 5, draw goes throughpsx_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.
psx_resource_frame_origin_ypsx_resource_frame_widthpsx_resource_frame_height
Resource-specific submitters:
psx_sprite_resource_submit_framepsx_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:
- type-4 descriptors bind a single image resource through
psx_resource_bind_single_image_vram_slot - 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_objectreads palette overrides from the original source-record pointer atobj + 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_8006769csubstrate/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_rootssection0_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 - xscreen_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_800758d4companion-extents metadata so later passes can replace placeholders without redoing the whole loader - do not treat
DAT_800758d4as 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*.WDLfamilies - separate authored record families instead of flattening them
- reconstruct multi-level
zvalues 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_800758d8bank - 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_800758d4now 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_800758d4as a packed per-state signed extents table - exported
stateLayerspreserve those decoded extents for each type - each exported scene item and
mapSourcerow now carries the resolvedcompanionExtentstuple 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_800758d4is 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.