Crusader_Decomp/docs/psx/map-rendering.md
2026-04-12 14:45:08 +02:00

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:

  1. A PSX level is not stored as one flat placement table.
  2. LSET*.WDL loads a multi-section bundle with at least three map-relevant classes of data:
    • authored root/dispatch records
    • authored constructor-placement records
    • per-type runtime banks for art, state scripts, variants, and simple-record payloads
  3. Constructors turn those authored rows into live objects with fixed-point world coordinates, a cached drawable resource pointer, a state-script bank pointer, a variant bank pointer, and a preserved pointer back to the original authored record.
  4. The runtime does not draw directly from the authored selector byte. It advances and sometimes reseats the live state script after spawn, then uses the current script word to drive frame choice and variant lookup.
  5. There are two separate world-facing render lanes:
    • stage 1: main visible-object list
    • stage 2: queued special-visible list
  6. The viewer can already reconstruct placement, projection, most resource loading, and much of the draw path from executable evidence.
  7. The main remaining blocker is the last live state-to-art rule for unresolved families such as 0x0042 and 0x0049. The map is still unreadable in practical terms because those families still fall back to placeholders.
  8. The current map 104 repeated-wall regression is now clearly over-merged. The cache shows type=0x0042 records from both authored section-0 families and multiple raw u5 lane/class values still collapsing onto the same donor wall bundle, which is stronger evidence of a wrong runtime family/resource merge than of a missed palette variant.
  9. The latest six-track Ghidra pass closes one more structural question: 0x0042 is not special at descriptor time. It shares the same generic descriptor cluster as the wider 0x003e..0x0050 band, so the practical split the viewer still needs is downstream in state, flags, lane routing, and resource kind.
  10. A focused live cleanup pass on the update and ordering lane now closes the missing-function/object gap around 0x80012b44: the parent routine is now explicitly modeled as psx_object_integrate_motion_and_route_visible (0x8001263c..0x80012c2c), with the local control helper named psx_object_update_runtime_input_modes (0x80012c30). The stage-1 ordering lane also now has explicit helper names/contracts for psx_main_visible_list_swap_entries (0x8002e064), psx_main_visible_order_graph_unlink_pair (0x8002ca74), and psx_main_visible_order_graph_detach_object (0x8002c89c), reducing ambiguity in rebucket/sort refresh behavior used by the exporter.
  11. The next concrete map 104 0x0042 pass now ties the exporter-side runtimeDiagnostic schema back to named live code instead of only to theory. The root and constructor section-0 families now have explicit named entry points but still converge through the same shared 0x0042 descriptor row, constructors are now proven to seed obj+0x1c by directly copying the authored lane word, the strongest recovered 0x0400 stage-selection write is still nested-state-side rather than a direct object-local 0x0042 writer, and DAT_800675f8 is now tighter as a level-loaded per-type policy pointer instead of a per-lane discriminator.
  12. A focused object-local route-bit provenance pass over the fixed map 104 0x0042 sample pack (item:25/30/31/35/85/86) now tightens the obj+0x1c & 0x0400 branch model: psx_object_create_simple_record (0x80024b48) and psx_object_create_compound_record (0x80025040) remain the strongest concrete object-local writers because they copy authored u5 directly into obj+0x1c; downstream named mutators (psx_type42_transition_selector_tick, psx_object_select_state_from_transition_table, psx_object_advance_state_script, psx_apply_deferred_control_to_live_objects, psx_object_handle_control_pair_0a) mutate other bits but do not introduce an object-local 0x0400 set/clear path; and recovered 0x0400 writers in this lane are still nested/global (psx_object_state_machine_dispatch_tick nested runtime word at 0x8001a078, global policy word in psx_object_handle_control_pair_0a at 0x80022a14).
  13. A focused visibility-routing/final-draw pass now closes the strongest remaining ambiguity between stage lanes and submitter rules. psx_object_integrate_motion_and_route_visible (0x800131a8) now has an explicit stage split note (type==4 || flags_1c&0x0400 routes to stage-2 special queue, else stage-1 main-visible), stage-1 order graph helpers are named/commented (0x8002c89c, 0x8002ca74, 0x8002d778, 0x8002e064), draw pass ordering is pinned (0x80041378), submitter dispatch is pinned (kind==5 image-table else sprite), and CLUT selection is now named through psx_clut_table_by_resource_bank (0x800a9f48) plus psx_clut_override_table_by_palette_token (0x800a9f66).

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 3497e7f641856415 on active writable SLUS_002.68
  • keep scope fixed to anchor groups already sampled in cache diagnostics:
    • root 0x0022: items 25/35
    • root 0x0030: items 30/31
    • constructor 0x0030: items 85/86
    • control 0x0066: item 53

Functions inspected in this pass (create/update/draw/control path):

  • psx_object_create_simple_record (0x800249f4) - edited (comment)
  • psx_object_create_compound_record (0x80024eec) - edited (comment)
  • psx_object_integrate_motion_and_route_visible (0x800131a8) - edited (comment)
  • psx_object_advance_state_script (0x80025d68) - edited (comment)
  • psx_draw_main_visible_object (0x80041458) - edited (comment)
  • psx_draw_special_visible_queue (0x80041144) - inspected (no new edit)
  • psx_main_visible_order_compare_pair_for_graph (0x8002be6c) - inspected (no new edit)
  • control-family callbacks (0x80013618, 0x80013688) - inspected (no new edit)

Live artifacts changed in this pass:

  • decompiler comments added/updated at:
    • 0x800249f4
    • 0x80024eec
    • 0x800131a8
    • 0x80025d68
    • 0x80041458

Concrete cohort conclusions (what still differs vs still fails to differ):

  1. Still differs (strong): authored route seed in section-0 records remains the cleanest stable split for this sample set. Constructor and root create paths both copy authored route word directly into obj+0x1c, so root 0x0022 and 0x0030 are preserved at spawn instead of being synthesized later.
  2. Still fails to differ (strong): root 0x0030 (30/31) and constructor 0x0030 (85/86) are route-equivalent at creation (obj+0x1c seed), so this pair should not be split by constructor-vs-root family alone.
  3. Still fails to differ (current capture): stage split bit obj+0x1c & 0x0400 remains clear in sampled anchors, so all cohorts continue through stage-1 main-visible rather than diverging by stage lane.
  4. Still fails to differ (current capture): no recovered per-cohort difference in submitter class at draw callsites yet; submitter remains bound-resource-kind based (kind==5 image-table else sprite) and current anchors lack a proven mixed-kind split.
  5. Still differs (secondary but real): main-visible draw injects authored palette-token high byte while special-visible does not. This remains a lane behavior split, but because sampled anchors currently route main-visible, it is not yet the primary divider among these specific cohorts.

Strongest evidence for the next exporter rule:

  • Use authored route seed (u5 -> obj+0x1c) as the first unresolved-family splitter for map-104 0x0042 placeholders, with 0x0022 and 0x0030 kept in separate fallback buckets until live capture proves convergence on resource kind plus latched frame token.

Safe immediate renderer/exporter change suggested by this pass:

  • keep same-type unresolved placeholders separated by authored route seed for this fingerprint:
    • bucket A: 0x0042 + route seed 0x0022
    • bucket B: 0x0042 + route seed 0x0030
  • do not split 0x0030 bucket by root vs constructor origin alone.
  • do not promote bit0x0400 or policy-word heuristics to primary keys for these anchors until runtime diagnostics actually sample non-null values.

Latest Loader/Install Pre-Constructor Pass (2026-04-12)

Pass objective:

  • close the pre-constructor loader/install side for graphics-critical state on active writable SLUS_002.68
  • recover concrete semantics for WDL bundle load, CLUT install, detached runtime-stream install, and per-type bank population
  • apply only conservative live Ghidra edits where evidence is direct from decompile/disassembly

Functions inspected (loader/install focus):

  • wdl_resource_bundle_load_by_index (0x80039444) - edited (comment)
  • psx_install_type_state_script_component_extents_banks (0x8003917c) - edited (comment)
  • psx_install_type_art_active_header_and_built_resource (0x80045ffc) - edited (comment)
  • level_palette_header_apply (0x8002badc) - edited (comment)
  • FUN_80040768 -> psx_install_level_audio_runtime_stream_bundle (0x80040768) - edited (rename + comment)

Live artifacts changed in this pass:

  • rename:
    • 0x80040768: FUN_80040768 -> psx_install_level_audio_runtime_stream_bundle
  • decompiler comments added/updated at:
    • 0x80039444
    • 0x8003917c
    • 0x80045ffc
    • 0x8002badc
    • 0x80040768

Recovered semantics (bundle/CLUT/runtime-bank install):

  1. wdl_resource_bundle_load_by_index performs a staged multi-section install before constructor dispatch: it reads per-level section sizes, installs type art/state lanes from two bundle passes, installs section-pack pointers (policy, control opcode stream, CLUT table), applies palette/CLUT header, installs detached runtime stream payload, optionally inflates persistent runtime-state blob, and only then dispatches root records.
  2. CLUT install is explicit in level_palette_header_apply: packed palette header fields are decoded, palette data is expanded, and CLUT blocks are uploaded through level_palette_upload_cluts while level CLUT mode/state globals are updated.
  3. Per-type state/runtime bank install is explicit in psx_install_type_state_script_component_extents_banks: it writes psx_type_state_script_bank, psx_type_simple_component_bank, and psx_type_companion_extents_bank by type row before constructors consume those lanes.
  4. Per-type art install is explicit in psx_install_type_art_active_header_and_built_resource: type art header slot is written, kind-4/5 resource is resolved/built, built-resource cache is committed, and the active slot is mirrored to resolved runtime resource for constructor-side binding.
  5. Detached runtime stream install is explicit in psx_install_level_audio_runtime_stream_bundle: it initializes SPU/sequence runtime from the detached blob header, maps 9 stream chunks, opens sequence/VAB handles, uploads stream payload to SPU RAM, and commits voice/channel defaults before object constructors run.

Strongest evidence for what is available before constructors run:

  • per-type art lane is already installed (psx_type_art_active_header_bank / psx_type_art_built_resource_bank)
  • per-type behavior/state lanes are installed (psx_type_state_script_bank, psx_type_simple_component_bank, psx_type_companion_extents_bank)
  • type-policy pointer table and control-opcode stream table are installed from section-pack offsets
  • level CLUT table and expanded/uploaded CLUT data are installed via palette header apply path
  • detached runtime stream payload (audio sequence/bank runtime) is installed and SPU runtime is initialized

Concrete exporter implication (avoid placeholder families):

  • export should model loader output as the authoritative pre-constructor state boundary, not only section-0 authored rows.
  • for unresolved visible families, placeholder fallback should be delayed until after reconstructing this install chain:
    • type art active/built lanes
    • type state/component/extents lanes
    • type policy lane
    • CLUT table + palette override readiness
  • practical consequence: resource-kind or frame-state donor heuristics that ignore preinstalled type/art/CLUT lanes will continue to collapse distinct runtime families into repeated placeholder walls.

Latest Draw Submission, Resource-Kind Dispatch, And Palette Token Pass (2026-04-12)

Pass objective:

  • close the visible draw submission side with direct evidence from the two world-visible lanes
  • pin exactly how bound resource kind and frame token select submitter and CLUT path
  • document exporter-facing implications for unresolved placeholder families

Functions inspected in this pass:

  • psx_draw_world_visible_passes (0x8004137c)
  • psx_draw_main_visible_object (0x80041458)
  • psx_draw_special_visible_queue (0x80041144)
  • psx_project_object_main_visible (0x80040d44)
  • psx_sprite_resource_submit_frame (0x80044bdc)
  • psx_image_table_submit_frame (0x80044e9c)

Live artifacts changed in this pass:

  • decompiler comments added/updated at:
    • 0x8004137c
    • 0x80041458
    • 0x80041144
    • 0x80040d44
    • 0x80044bdc
    • 0x80044e9c

Recovered semantics (draw submission and palette token):

  1. World draw order is fixed by psx_draw_world_visible_passes: stage-1 sorted main-visible draw first, then stage-2 special-visible queue, then HUD/overlay.
  2. Both visible world lanes use the same resource-kind dispatch at draw call sites:
    • if **(obj+0x10) == 5 -> psx_image_table_submit_frame
    • else -> psx_sprite_resource_submit_frame
  3. Main-visible injects authored palette token into submit flags before dispatch:
    • base flags include obj_flags & 0x0002
    • for type 0x003e..0x00ab: token high byte from source+0x06
    • for type >= 0x00ac: token high byte from source+0x0c
  4. Special-visible does not inject authored palette token high byte; it passes only orientation/flip bits (obj_flags & 0x0002) into submitters.
  5. Submitters converge on the same CLUT rule:
    • default: psx_clut_table_by_resource_bank[resource_bank]
    • override when high-byte token present: psx_clut_override_table_by_palette_token[(submit_flags >> 8)]
  6. Frame token bridge is explicit in projection and submit:
    • obj+0x94 is used by psx_project_object_main_visible frame-origin/size helpers
    • same token is passed to sprite/image-table submitters as the per-object visible frame selector

Strongest main-visible vs special-visible evidence split:

  • Main-visible (0x80041458) computes and ORs a high-byte palette token before submit.
  • Special-visible (0x80041144) performs the same kind dispatch but omits token injection.
  • Because submitters only apply CLUT override when nonzero high-byte token exists, this omission is the strongest executable-backed reason the two lanes can render with different palette behavior even for comparable resource kinds.

Concrete exporter implication for remaining placeholder families:

  • Keep world visible lanes distinct in export/replay and diagnostics:
    • lane main-visible: allow authored palette-token override into CLUT selection.
    • lane special-visible: do not apply authored token override unless a separate stage-2 token source is recovered.
  • For unresolved families (0x0042, 0x0049, 0x0055..0x0063), include both fields in runtime diagnostics and donor-key matching:
    • bound resource kind (sprite vs image-table path)
    • latched frame token (obj+0x94)
  • Avoid cross-kind donor fallback (sprite <-> image-table) because submitter path and frame metadata semantics differ at callsite level.

Latest Selector/Transition Pre-Latch Pass (2026-04-12)

Pass objective:

  • close pre-latch selector and transition-row semantics for unresolved visible families, centered on type 0x0042
  • separate early gating and selector reseat (obj+0x9e path) from final frame latch (obj+0x94 path)

Functions inspected in this pass:

  • psx_type42_transition_selector_tick (0x80018578)
  • psx_object_select_state_from_transition_table (0x8001bca0)
  • psx_object_is_within_view_margin (0x8001e6e8)
  • psx_object_select_state_script (0x800260e8)
  • psx_object_advance_state_script (0x80025d68)

Live artifacts changed in this pass:

  • decompiler comments added/updated at:
    • 0x80018578
    • 0x8001bca0
    • 0x8001e6e8
    • 0x800260e8
    • 0x80025d68

Recovered selector/transition semantics (exact current read):

  1. Type-0x0042 pre-latch reseat is explicitly gated before selector edits: psx_type42_transition_selector_tick first requires psx_object_is_within_view_margin(obj, 0x14) and object lane bit obj+0x1c & 0x0020 before its turn/heading reseat path proceeds.
  2. Transition row selection is table-indexed and type-relative in psx_object_select_state_from_transition_table: transition_code = DAT_80063a00[(type-0x1e)*0x0f + slot], then selector base from DAT_80063b4c[(type-0x1e)*0x0f + transition_code].
  3. For type 0x0042, the recovered rows are now byte-concrete from live memory:
    • mode/policy row @ 0x80063c1c: 2d 00 00 05 0a 14 0f 19 23 23 28 00 00 00 1e
    • selector row @ 0x80063d68: 3c 00 00 00 50 0f 00 00 00 19 00 00 32 32 00
  4. Selector 3/4 (and neighboring headings) feed art choice through the pre-latch path, not by directly writing the final frame token: turn/heading branches in 0x80018578 call psx_object_select_state_script with wrapped heading buckets (& 7) before latch.
  5. psx_object_select_state_script is confirmed as selector install only (obj+0x9e, obj+0x8c/0x90 cursor); final visible token still latches later in psx_object_advance_state_script via write to obj+0x94.

Strongest evidence for selector-to-visible-art linkage:

  • The same live chain now stays consistent across all inspected callsites: pre-latch selector reseat (0x80018578 and 0x8001bca0) -> selector install (0x800260e8) -> frame/state latch (0x80025d68, write to obj+0x94) -> projection/draw frame queries. This is the clearest current evidence that selector 3/4 effects are real but indirect: they bias the later latched frame token rather than bypassing it.

Exporter implication for map 104 and remaining placeholder families:

  • Treat selector and latch as separate channels in export logic for unresolved families (0x0042, 0x0049, 0x0055..0x0063):
    • preLatchSelector: from reseat/install lane (obj+0x9e path)
    • latchedFrameToken: final draw-driving token (obj+0x94)
  • Do not collapse unresolved cases into one donor wall when pre-latch selectors differ but latches are unknown. For map 104, this pass further supports splitting placeholder cohorts by selector/transition row behavior before any cross-family donor fallback.

Latest Type-Art Install And Constructor-Bind Pass (2026-04-12)

Pass objective:

  • tighten the active-header/built-resource install lane around 0x800758d8 and 0x800758c8
  • lock a conservative, evidence-backed install->constructor->draw chain for unresolved map-viewer families

Functions inspected (focused set):

  • psx_install_type_art_active_header_and_built_resource (0x80045ffc)
  • psx_object_create_simple_record (0x800249f4)
  • psx_object_create_compound_record (0x80024eec)
  • psx_stream_install_type_runtime_banks (0x80038f18)
  • psx_create_image_resource_from_descriptor (0x80044434)
  • psx_draw_main_visible_object (0x80041458)

Live artifacts changed in this pass:

  • decompiler comments added/updated at:
    • 0x80045ffc
    • 0x800249f4
    • 0x80024eec
    • 0x80038f18
    • 0x80041458

Strongest install->draw evidence recovered:

  1. psx_install_type_art_active_header_and_built_resource first writes the incoming header pointer into psx_type_art_active_header_bank[type] (0x800758d8 lane), then performs kind dispatch (4 single-image bind, 5 bundle build/upload) and commits the resolved runtime resource pointer into psx_type_art_built_resource_bank[type] (0x800758c8 lane).
  2. The same installer mirrors the active slot to the resolved runtime resource pointer after build/reuse, so constructor-time reads observe resolved per-type runtime resource state, not only raw descriptor headers.
  3. Both constructors (simple and compound) bind obj+0x10 from the per-type lane: if active entry kind is 5, they reuse psx_type_art_built_resource_bank[type]; otherwise they invoke psx_create_image_resource_from_descriptor and refresh cache state.
  4. psx_stream_install_type_runtime_banks seeds/clears type runtime bank slots and resets built-resource cache entries during stream install, preserving a clear ownership boundary between stream payload install and later resource realization.
  5. psx_draw_main_visible_object uses ctor-bound obj+0x10 plus live frame token obj+0x94; submitter choice is resource-kind based (kind==5 -> psx_image_table_submit_frame, else psx_sprite_resource_submit_frame).

Conservative semantics confirmed in this pass:

  • 0x800758d8 is the active per-type art lane at install/constructor handoff time.
  • 0x800758c8 is the built-resource cache lane used for kind-5 reuse and refreshed through install/build paths.
  • Constructor binding and draw submission are directly connected through obj+0x10 (resource) and obj+0x94 (live frame token), so unresolved visible families should be debugged as runtime lane/state/resource issues rather than as missing top-level section-0 decode.

Exporter-facing implication for unresolved map-104 families:

  • For unresolved 0x0042 bands, do not treat type-level art as a static type -> bundle table. The executable path is type install -> active/built per-type lane -> ctor object bind -> live frame token -> kind-based submitter.
  • Mixed-family donor heuristics should remain fenced unless they match this chain (especially resource kind parity and frame-state behavior), because map-104 failures are now better explained by wrong runtime resource-family merges than by missing section-0 placement decoding.

Latest Visibility Routing, Ordering, And Draw-Lane Pass (2026-04-12)

Pass objective:

  • close final routing/order/submitter ambiguity around stage-1 versus stage-2 world draw lanes
  • keep edits conservative and evidence-backed in live SLUS_002.68

Functions and helpers inspected in this pass:

  • routing/input lane:
    • psx_player_object_integrate_motion_and_route_visible (0x8001263c, renamed this pass from duplicate generic name)
    • psx_object_update_runtime_input_modes (0x80012c30)
    • psx_object_integrate_motion_and_route_visible (0x800131a8)
  • stage-1 order graph lane:
    • psx_main_visible_order_graph_detach_object (0x8002c89c)
    • psx_main_visible_order_graph_unlink_pair (0x8002ca74)
    • psx_main_visible_list_sort_range (0x8002d778)
    • psx_main_visible_list_swap_entries (0x8002e064)
  • draw and submit lane:
    • psx_draw_world_visible_passes (0x80041378)
    • psx_draw_special_visible_queue (0x80041144)
    • psx_draw_main_visible_object (0x80041458)
    • psx_sprite_resource_submit_frame (0x80044bdc)
    • psx_image_table_submit_frame (0x80044e9c)

Live edits applied in this pass:

  • rename:
    • 0x8001263c: psx_object_integrate_motion_and_route_visible -> psx_player_object_integrate_motion_and_route_visible
  • data labels:
    • 0x800a9f48 -> psx_clut_table_by_resource_bank
    • 0x800a9f66 -> psx_clut_override_table_by_palette_token
  • decompiler comments:
    • 0x800131a8: explicit stage split (type==4 || flags_1c&0x0400 -> stage-2 special queue)
    • 0x8002d778: stage-1 dependency sorter and policy-bit influence (0x0008, 0x0600)
    • 0x8002ca74: directed order-edge unlink contract
    • 0x8002c89c: full order-graph detach contract
    • 0x80041378: fixed world draw pass sequence (stage-1, stage-2, then HUD)
    • 0x80041458: main-visible submitter and palette-token override rule
    • 0x80041144: stage-2 submitter rule parity, no authored palette-token override
    • 0x80044bdc: sprite submit upload/refresh and CLUT override behavior
    • 0x80044e9c: image-table submit metadata-only frame resolve and CLUT override behavior

Strongest evidence recovered:

  1. Stage routing split in 0x800131a8 is explicit and branch-local: type==4 || (obj+0x1c & 0x0400) calls psx_project_object_special_visible_queue; the fallthrough path calls psx_project_object_main_visible.
  2. World pass submission order in 0x80041378 is explicit and fixed: iterate sorted main-visible slice -> draw special-visible queue -> draw HUD/overlay.
  3. Stage-1 ordering is not a plain z-sort: 0x8002d778 repeatedly resolves dependency counts and may call psx_main_visible_order_graph_unlink_pair; graph maintenance is handled by 0x8002ca74 and bulk detach by 0x8002c89c.
  4. Submitter choice is lane-independent and strictly resource-kind based at both call sites (0x80041458, 0x80041144): kind==5 routes to psx_image_table_submit_frame; else psx_sprite_resource_submit_frame.
  5. Palette handling differs by lane: main-visible computes an authored high-byte token from source fields (+0x06 for 0x003e..0x00ab, +0x0c for >=0x00ac) and ORs it into submit flags, while special-visible currently passes only orientation-flip bit (no authored token injection).
  6. Both submitters converge on identical CLUT override semantics: default from psx_clut_table_by_resource_bank, optional override through psx_clut_override_table_by_palette_token[(flags>>8)] when high-byte token is present.

Renderer implication for map-viewer quality:

  • stage-1/stage-2 should remain distinct queues through export and replay; merging them collapses ordering and palette behavior.
  • ordering must preserve graph constraints, not only depth heuristics.
  • frame submitter must be selected from bound resource kind (5 image-table, otherwise sprite) independent of lane.
  • palette override behavior should be lane-aware: apply authored token override in main-visible only; keep stage-2 on baseline CLUT unless separate evidence introduces a stage-2 override source.

2026-04-12 Exporter Follow-Through

The renderer/cache builder now follows two of the strongest executable-backed constraints from the visibility/draw pass:

  1. Authored palette override bytes are only consumed for records whose authored route flags currently classify them as main-visible. If a record carries a palette token but routes to the special-visible lane, scene export now preserves that token only as diagnostics and keeps the rendered palette on the bundle-default/heuristic path.
  2. Cross-map donor reuse is now fenced by recovered active-header payloadKind from DAT_800758d8, so the exporter no longer treats image-table-backed and sprite-backed types as interchangeable donor candidates when borrowing unresolved art.

That does not close the remaining unresolved families by itself, but it removes two broad sources of false positives:

  • stage-2 objects inheriting authored palette overrides that the executable does not inject
  • unresolved types borrowing donors from the wrong resource-kind family

Latest Runtime/Control-Island Policy Pass (2026-04-12)

Pass objective:

  • test whether the runtime/control island around 0x80063e54 / 0x80063e68 / 0x800675ec and per-type policy table 0x800675f8 can directly explain unresolved visible-art family splits on map 104
  • apply only conservative live edits for helpers where behavior is directly evidenced

Functions inspected in this pass (focus set):

  • psx_level_post_load_runtime_reset (0x80039ef4) - edited (comment)
  • psx_object_integrate_motion_and_route_visible (0x800131a8, policy read at 0x8001353c) - edited (comment)
  • psx_draw_main_visible_object (0x80041458, policy read at 0x80041604) - edited (comment)
  • psx_main_visible_order_compare_pair_for_graph (0x8002bf0c) - edited (comment)
  • passcode/control state block around 0x80034d60 (table-pair control gate) - edited (disassembly comment)

Conservative helper renames landed (runtime-marker island family):

  • 0x8002e598: FUN_8002e598 -> psx_marker_channel_runtime_state_init_heap_block
  • 0x8002e484: FUN_8002e484 -> psx_marker_channel_get_mode2_meter_value
  • 0x8002e498: FUN_8002e498 -> psx_marker_channel_try_spend_mode2_meter
  • 0x8002e3e8: FUN_8002e3e8 -> psx_marker_channel_add_mode2_meter_and_queue_event
  • 0x800308ac: FUN_800308ac -> psx_marker_channel_refresh_mode2_active_slot
  • 0x800304c4: FUN_800304c4 -> psx_marker_channel_cycle_active_slot_by_direction

Recovered island/policy interaction semantics (strongest current read):

  1. 0x80063e68 and 0x80063e54 act as a reciprocal control-gating pair in post-load/passcode transition flow: selected map id maps to slot via 0x80063e68, then is validated by reverse map lookup via 0x80063e54 before mode-action side effects proceed.
  2. 0x800675ec is the marker-channel runtime-state block pointer used by loadout/mode-action/reset and marker event helpers; this is a control/runtime lane, not a direct per-object art-resource lane.
  3. 0x800675f8 policy words are consumed in routing/order/draw as behavior bits by type:
    • route-side read (0x80013550) uses 0x1000 for nearby-interaction publication behavior after lane route decision
    • draw-side read (0x8004161c) uses 0x2000 for a render-state branch after submitter/lane are already chosen
    • order-graph compare (0x8002bf2c) uses 0x0600 class behavior during pair ordering

Strongest evidence for/against this being the visible-art family split:

  • Against direct lane-split causality (strong): stage-1 vs stage-2 world-visible lane choice is object-local obj+0x1c & 0x0400 at 0x80013518, not a direct read of 0x800675f8 or the 0x63e54/0x63e68/0x675ec island.
  • Against direct submitter-split causality (strong): sprite vs image-table submitter selection is resource-kind based at submit sites, before/independent of policy bit tests.
  • For secondary visual influence (moderate): policy bits (0x2000, 0x0600, 0x1000) can still change render-state branch, draw ordering class, and interaction publication. These can alter final presentation behavior but are not currently the primary discriminator for unresolved family resource/lane identity.

Concrete exporter implication for unresolved map 104 cohorts:

  • keep island/policy diagnostics, but do not use them as first-key art-family split selectors:
    • primary split keys remain bound resource kind, obj+0x94 latched frame token, and route bit obj+0x1c & 0x0400
    • island/policy words should be recorded as secondary modifiers (orderingClass, drawPolicyBits, publishPolicyBits) to explain same-resource visual divergence
  • practical fallback rule: if cohorts differ only by island/policy bits and not by resource kind/frame token/route lane, treat them as one art family with policy-variant rendering, not separate base-art families.

Latest Storage-Mapping Pass (2026-04-11)

Pass objective:

  • stay on live PSX storage ownership and runtime-bank install chain for unresolved map 104 type 0x0042
  • avoid draw-path broadening and focus on subordinate table/blob ownership

Functions and globals inspected in this pass:

  • loader and blob chain:
    • wdl_resource_bundle_load_by_index (0x80039444)
    • psx_lzss_unpack_into_level_buffer (0x8003b00c, renamed this pass)
    • psx_lzss_pack_level_buffer (0x8003aba8, renamed this pass)
    • psx_load_type_state_banks (0x8003917c)
    • 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-0x0042 descriptor row pointer/value anchors:
      • 0x80063220 -> 0x800626f8
      • row callbacks include psx_spawn_compound_record_advance_state_once (0x80013618), psx_spawn_simple_record_set_active_flag (0x8001372c), and update/release slots
  • constructor/runtime-bank consumers:
    • psx_object_create_simple_record (0x800249f4)
    • psx_object_create_compound_record (0x80024eec)
    • psx_draw_main_visible_object (0x80041458)
    • psx_family_wrapper_spawn_compound_pair_y_and_type42_mode_gate (0x800230e4)
  • key storage globals:
    • psx_level_decompressed_state_buffer (0x8006769c)
    • psx_level_state_compressed_blob (0x8006b5d8, renamed this pass)
    • psx_level_heap_cursor (0x8006763c, renamed this pass)
    • psx_section0_dispatch_root_records (0x80067720)
    • psx_section0_constructor_placement_records (0x800678f0)
    • DAT_80067938, DAT_80067838, DAT_80067840, DAT_800676d8
    • psx_type_policy_table_ptr (0x800675f8)
    • psx_type_art_active_header_bank (0x800758d8)
    • psx_type_art_built_resource_bank (0x800758c8)
    • psx_type_state_script_bank (0x800758cc)
    • psx_type_simple_component_bank (0x800758d0)
    • psx_type_companion_extents_bank (0x800758d4)

Applied live renames/comments in this pass:

  • renames:
    • FUN_8003b00c -> psx_lzss_unpack_into_level_buffer
    • FUN_8003aba8 -> psx_lzss_pack_level_buffer
    • DAT_8006b5d8 -> psx_level_state_compressed_blob
    • DAT_8006763c -> psx_level_heap_cursor
  • comments:
    • at 0x80039af0: compressed level-state blob loads into psx_level_state_compressed_blob, then unpacks into psx_level_decompressed_state_buffer before root/constructor dispatch
    • at 0x800249f4: constructor bind note for psx_type_art_active_header_bank[type] and psx_type_art_built_resource_bank[type]

Recovered ownership chain (strongest current read):

  1. wdl_resource_bundle_load_by_index reads the level section header, allocates contiguous level heap storage, and installs section pointers (psx_section0_dispatch_root_records, psx_section0_constructor_placement_records, plus subordinate slices such as DAT_80067938, DAT_80067838, psx_type_policy_table_ptr, DAT_80067840, DAT_800676d8).
  2. The same loader reads compressed state into psx_level_state_compressed_blob and unpacks through psx_lzss_unpack_into_level_buffer into psx_level_decompressed_state_buffer (DAT_8006769c), then proceeds with root/constructor section dispatch setup.
  3. Type runtime banks are installed from subordinate bundle sections: psx_load_type_state_banks seeds psx_type_state_script_bank, psx_type_simple_component_bank, and psx_type_companion_extents_bank; descriptor/resource install paths seed psx_type_art_active_header_bank and psx_type_art_built_resource_bank.
  4. Section-0 authored records feed object construction via descriptor-table callback rows. For 0x0042, both root and constructor families resolve through 0x80063220 -> 0x800626f8 and enter the same generic create/update/release family.
  5. During object creation and presentation, runtime art/state ownership is bank-driven, not direct-authored-draw-driven: constructors bind through psx_type_art_active_header_bank[type] and psx_type_art_built_resource_bank[type], while draw/update logic also consumes psx_type_policy_table_ptr[type] policy bits.

Storage nodes still unresolved (blocking full map 104 0x0042 closure):

  1. exact schemas and semantics for DAT_80067938, DAT_80067838, DAT_80067840, and DAT_800676d8 (currently installed by offset in the section pack but weakly typed downstream)
  2. map-104-specific ownership/meaning of psx_level_decompressed_state_buffer payload slices after inflate (which words materially feed type-0x0042 object family divergence)
  3. explicit per-item runtime correlation for sample set 25/30/31/35/85/86: bound resource pointer identity, resource->kind, and latched frame/state channels against shared 0x0042 descriptor row
  4. final control interaction between section-installed policy (psx_type_policy_table_ptr), constructor-seeded obj+0x1c flags, and any deferred-control reads from subordinate tables before submit

Latest Object-Local 0x0400 Provenance Pass (2026-04-11)

Focused Type-Policy Provenance Pass (2026-04-11)

Focused target:

  • DAT_800675f8 / psx_type_policy_table_ptr
  • concrete map-104 relevance for type 0x0042 and control type 0x0066
  • installation source, reader masks, and storage ownership limits

Functions/addresses inspected in this pass:

  • writer/install path:
    • wdl_resource_bundle_load_by_index (0x80039444), write at 0x800398f0
  • key readers sampled directly:
    • psx_object_integrate_motion_and_route_visible (0x8001353c) -> & 0x1000
    • psx_draw_main_visible_object (0x80041604) -> & 0x2000
    • psx_main_visible_order_compare_pair_for_graph (0x8002bf0c, 0x8002c174, 0x8002bf5c) -> & 0x0600
    • psx_object_state_machine_dispatch_tick (0x8001a280, 0x8001aa44) -> & 0x0800
    • psx_object_update_nearby_interactions (0x8002957c, 0x80029970, etc.) -> & 0x0100, & 0x4000, & 0x0008
    • psx_object_register_contact_pair / related overlap helpers (0x80028488 family) -> & 0x0008
    • psx_find_nearest_los_target_with_typeflag10_or20 (0x800149cc, 0x80014b14) -> & 0x0010, & 0x0020
    • psx_update_nearest_policy80_contact_marker (0x8001408c) -> & 0x0080

Applied live symbol/comment updates (small and evidence-backed):

  • rename:
    • FUN_8001408c -> psx_update_nearest_policy80_contact_marker
  • comments:
    • 0x800398f0: marks psx_type_policy_table_ptr install from level section pack (DAT_80067838 + local_b4)
    • 0x8002bf0c: marks 0x0600 class compare role in stage-1 ordering
    • 0x80041604: marks 0x2000 draw-time semitrans gate
    • 0x800140c8: marks 0x0080 policy gate before proximity trace selection

Best evidence on provenance and storage ownership:

  1. psx_type_policy_table_ptr has exactly one recovered writer in the executable (0x800398f0) and is rebuilt during wdl_resource_bundle_load_by_index.
  2. The installed pointer is computed from the contiguous map bundle section pack (DAT_80067838 + local_b4), with subsequent slices derived by offset (+ local_b0, + local_ac, + local_a8).
  3. This establishes ownership as level-loaded runtime storage (LSET bundle section data), not immutable executable ROM data.
  4. Static executable-only recovery of concrete policy rows for type 0x0042 / 0x0066 remains blocked because the table contents are not authored in-place in the code image.
  5. Runtime-adjacent cache evidence is still incomplete for these rows: current map-104 sample rows (25/30/31/35/85/86 for 0x0042, 53 for 0x0066) still export runtimeDiagnostic.typePolicy.sampled=false and word=null.

Current interpretation impact for map-104 0x0042 art split:

  • psx_type_policy_table_ptr[type] is now more strongly confirmed as shared per-type policy gating (interaction, ordering, and draw flags), not a per-lane constructor-family selector.
  • This table should stay secondary for explaining the 0x0042 64x64 vs 64x40 split until concrete map-level row capture is available; primary discriminators remain resource identity, pre-latch/latch state flow, and route/state bits.

Focused target:

  • route branch inside psx_object_integrate_motion_and_route_visible: obj+0x1c & 0x0400
  • fixed sample families: root u5=0x0022, root u5=0x0030, constructor u5=0x0030
  • fixed sample pack: item:25/30/31/35/85/86

Concise provenance summary (inspected writer/preserve/transform chains):

  • Object-local writers (direct obj+0x1c stores):

    • psx_object_create_simple_record (0x80024b48) — copies authored u5 into obj+0x1c at creation (strong object-local seed).
    • psx_object_create_compound_record (0x80025040) — same copy behavior for compound constructors.
  • Named mutators examined (mutate low control bits, none introduce direct 0x0400 set/clear):

    • psx_type42_transition_selector_tick
    • psx_object_select_state_from_transition_table
    • psx_object_advance_state_script
    • psx_apply_deferred_control_to_live_objects
    • psx_object_handle_control_pair_0a
  • Strongest recovered non-object-local 0x0400 writers (nested/runtime or policy-driven):

    • psx_object_state_machine_dispatch_tick (0x8001a078) — nested runtime-state write.
    • psx_object_handle_control_pair_0a (0x80022a14) — global/policy-adjacent write affecting routing control words.

Conclusion: constructors remain the primary object-local seed for obj+0x1c (including 0x0400 when authored), but the strongest dynamic 0x0400 writes seen so far are nested/runtime or policy-driven and not direct object-local transforms. A focused live runtime capture on the fixed sample pack is still required to conclusively separate authored 0x0400 from nested/policy-set 0x0400 during the psx_object_integrate_motion_and_route_visible branch.

Addresses inspected in this pass:

  • route-side consumer and branch context: 0x8001263c..0x80012c2c, 0x800131a8..0x80013614
  • constructor/local writers: 0x80024b48, 0x80025040, 0x80031514
  • transition/mutator lanes touching obj+0x1c: 0x8001918c, 0x800191b0, 0x800191f4, 0x8001bde8, 0x80025dd4, 0x80025e88, 0x8002b0c8, 0x8002b174, 0x80022b28
  • non-object-local 0x0400 controls in same neighborhood: 0x8001a078, 0x80022a14

Recovered chain for object-local 0x0400 in this sample lane:

  1. Section-0 authored row feeds constructor.
  2. Constructor copies authored u5 directly into obj+0x1c (psx_object_create_simple_record / psx_object_create_compound_record).
  3. Later named mutators in the 0x0042 path rewrite low control bits (0x0001/0x0002/0x0004/0x0020/0x0040/0x0080/0x0100/0x0200) but no inspected object-local write site introduces a new obj+0x1c |= 0x0400 or obj+0x1c &= ~0x0400 transform.
  4. Route split still consumes obj+0x1c & 0x0400 at psx_object_integrate_motion_and_route_visible.

Current strongest read stays unchanged but tighter:

  • strongest object-local writer for 0x0400 presence/absence is still authored u5 copied at object creation
  • strongest 0x0400 writer recovered in active runtime logic remains nested/global state, not object-local obj+0x1c

Implication for map 104 0x0042 remains open but narrower:

  • yes, live capture is still needed to close the gap for item:25/30/31/35/85/86 because static writer recovery still does not pin a direct object-local runtime transformer for bit 0x0400

Latest Table Inventory Pass (2026-04-11)

This pass stayed broad on table coverage but concrete on evidence, centered on unresolved map 104 / type 0x0042 storage and render routing context.

Functions/data inspected in this pass:

  • Descriptor and transition tables:
    • 0x80063118 (psx_type_descriptor_table base)
    • 0x80063220 (psx_type_descriptor_ptr_0042)
    • 0x80063b4c (psx_type_transition_selector_rows)
  • Type-policy and runtime-bank globals:
    • 0x800675f8 (psx_type_policy_table_ptr)
    • 0x800758c8 (psx_type_art_built_resource_bank)
    • 0x800758d0 (psx_type_simple_component_bank)
    • 0x800758d4 (psx_type_companion_extents_bank)
    • 0x800758d8 (psx_type_art_active_header_bank)
  • Marker/runtime control island near post-load reset:
    • 0x80063e68 (DAT_80063e68)
    • 0x80063e54 (DAT_80063e54)
    • 0x800675ec (DAT_800675ec)
    • 0x80067728 (current level index)
  • Key functions touched during inventory/decompile:
    • psx_object_create_simple_record
    • psx_object_create_compound_record
    • psx_dispatch_section0_dispatch_roots
    • psx_dispatch_section0_constructor_placements
    • psx_run_live_object_type_updates
    • psx_object_select_state_from_transition_table
    • psx_object_integrate_motion_and_route_visible
    • psx_draw_main_visible_object
    • psx_cache_type_art_descriptor_and_resource
    • psx_level_post_load_runtime_reset
    • psx_section0_dispatch_root_seed_marker_channel_table

Applied live symbol updates (small, evidence-backed):

  • 0x8002f190 -> psx_marker_channel_dispatch_mode_action
  • 0x8002f250 -> psx_marker_channel_runtime_state_clear
  • 0x80031840 -> psx_marker_channel_mode_is_enabled
  • 0x8003185c -> psx_marker_channel_get_mode_step_value
  • 0x80031878 -> psx_marker_channel_runtime_state_snapshot
  • 0x80031a3c -> psx_marker_channel_runtime_state_restore

Applied targeted comments:

  • 0x80063e68: per-level marker-channel profile seed byte indexed by current level index (DAT_80067728) and consumed by seed/apply/reset flow.
  • 0x800675ec: pointer to 0x90-byte marker-channel runtime state block shared by clear/snapshot/restore/accessor paths.
  • 0x80063e54: per-level companion byte table read by post-load reset and adjacent control helper, likely paired with DAT_80063e68 as profile/eligibility control.

Concrete table-role clarifications from this pass:

  • Type 0x0042 still resolves through generic descriptor table fetch at psx_type_descriptor_table[0x0042] (no unique descriptor fork).
  • The transition selector row table at 0x80063b4c remains narrow and state-chooser specific (only direct reads in psx_object_select_state_from_transition_table).
  • psx_type_policy_table_ptr (0x800675f8) remains a draw/routing policy source (including semitrans and ordering/publication gating) rather than a unique map-104 art discriminator.
  • Runtime-bank trio (0x800758d0/0x800758d4/0x800758d8) is confirmed as install/reset-backed loader products consumed by constructors and downstream draw/resource setup.
  • The 0x80063e54/0x80063e68/0x800675ec cluster is now tighter as a level-indexed marker/control runtime island that can affect post-load state and channel behavior, and is therefore a legitimate remaining table family for map-104 type-0x0042 divergence work.

Latest Table-Typing Pass (2026-04-11)

This pass stayed focused on turning the newly named storage/control pointers into actual table roles instead of broadening back out into generic render theory.

Applied live symbol updates in this pass:

  • DAT_80067938 -> psx_ctor_placement_section_ptr
  • DAT_800676d8 -> level_clut_table_ptr
  • DAT_80067840 -> psx_control_opcode_stream_table
  • DAT_80063e54 -> psx_level_selector_table_80063e54
  • DAT_80063e68 -> psx_level_channel_table_80063e68

And the DAT_800675ec rename from the previous batch now has a stronger local field map as psx_marker_channel_runtime_block.

psx_ctor_placement_section_ptr is a real section-pack root, but not the final row family by itself

The top consumer pair (wdl_resource_bundle_load_by_index and psx_apply_deferred_control_command) now shows that psx_ctor_placement_section_ptr is installed as a section root and then used to derive subordinate bases such as section_pack_source_80067838.

The strongest current consumer path is not the original six-halfword constructor row itself. Instead it uses a small u16 index table rooted near psx_ctor_placement_section_ptr - 2, multiplies the selected index by 8, and then walks 8-byte rows out of the derived section_pack_source_80067838 base.

So the safe current read is:

  • psx_ctor_placement_section_ptr is the constructor-placement section root installed at level load
  • some downstream control/deferred-command logic uses it as a header/index root for a different 8-byte row family
  • no direct type == 0x0042 comparison was recovered in this consumer pair yet

That narrows the next storage question again: distinguish the original constructor-placement row family from the subordinate control/index rows derived from the same section root.

level_clut_table_ptr is palette/CLUT support, not the missing 0x0042 art table

The current best evidence from level_palette_expand_5bit_to_16color and level_palette_upload_cluts is that level_clut_table_ptr is a halfword array whose low 5 bits are used as a CLUT index (0..31).

That keeps this table on the level-palette side of the pipeline:

  • useful for explaining color/CLUT choice
  • not a strong candidate for the remaining 0x0042 shape/frame split by itself

So map-104 0x0042 should still treat level_clut_table_ptr as palette support rather than the missing world-object family discriminator.

psx_control_opcode_stream_table is control/state support, not a direct world-art selector

The writer at 0x80039908 and the strongest reader at psx_control_assign_opcode_stream_by_index now tighten DAT_80067840 into a real control/opcode-stream pointer table installed from the level bundle.

Its current best role is:

  • primary: state/control opcode stream support for constructor/state-machine callers
  • secondary: indirect behavioral influence on presentation through control/state logic
  • not a direct art/frame lookup table for unresolved 0x0042

That means it still matters for map behavior, but it is not currently the best direct explanation for 64x40 versus 64x64 presentation.

psx_marker_channel_runtime_block is now narrowed to marker/control runtime state with two still-interesting short fields

The latest field pass keeps most of this 0x90-byte block on the marker/control side:

  • +0x34 = mode byte
  • +0x6c = per-mode/per-step byte
  • +0x88 / +0x8c = restored/snapshotted short fields still worth treating as the most presentation-adjacent members in this struct

Snapshot/restore code now clearly reads and writes +0x88/+0x8c through global snapshot words before restoring them to the runtime block. That proves they are part of live runtime state, but not yet that they feed ordinary world-object draw.

So this family stays important, but the open question is narrower again: do +0x88/+0x8c ever flow into actual render/presentation logic, or do they remain marker/control-only state.

psx_level_selector_table_80063e54 and psx_level_channel_table_80063e68 are per-level control tables

The read windows in psx_level_post_load_runtime_reset and psx_section0_dispatch_root_seed_marker_channel_table now establish both addresses as per-level byte tables, not scalars.

Current best read:

  • psx_level_channel_table_80063e68 is used by section-0 marker/channel seeding and related dispatch control
  • psx_level_selector_table_80063e54 is a paired per-level selector/index byte consumed in the same post-load/control island

This keeps the whole 0x63e54/0x63e68/0x675ec family grouped as one legitimate remaining blocker family for level-specific control/state behavior.

Install-chain consequence

The install-helper pass also tightened the unpack flow one step further:

  • psx_level_decompressed_state_buffer is still the unpack destination and immediate source for runtime-bank installation
  • psx_load_type_state_banks remains the installer for DAT_800758cc/d0/d4
  • psx_stream_install_type_runtime_banks remains the packed-stream installer that can populate the art/built-resource side too
  • constructors then consume those installed banks directly when binding drawable resources

So the remaining storage/render gap is now tighter than before:

  • not “what installs the runtime banks”
  • not “is 0x0042 hiding behind one unnamed unique descriptor fork”
  • but “which installed rows and control tables still explain the concrete map-104 0x0042 family split after load”

Highest-value unresolved tables/structures after this pass:

  1. DAT_800675ec struct layout (0x90 bytes): fields at +0x34 bit-flag rows, +0x6c step-value rows, and +0x84..+0x8f state words still need a named datatype.
  2. DAT_80063e68 and DAT_80063e54 exact schema (entry count/semantics) and whether they directly separate map-104 0x0042 families or only gate marker/control channels.
  3. Type-0x0042 runtime-bank interpretation at constructor and post-state-advance points: exact relationship between DAT_800758d8[type] active header, DAT_800758c8[type] built resource, and effective submitted frame group for sample items 25/30/31/35/85/86.
  4. Per-type policy word capture for 0x0042 in live map-104 sample context (psx_type_policy_table_ptr[0x0042]) correlated with stage route (obj+0x1c / stage-1 vs stage-2) and bound resource kind.

Latest Live Cleanup (2026-04-11)

Focused live SLUS_002.68 cleanup tightened the world-frame/render wrapper lane around 0x80031f0c, 0x80031f9c, 0x800320bc, 0x80039dc4, 0x8003977c, and 0x800391f0.

Applied in-database symbol cleanups:

  • 0x80031e0c -> psx_lset_session_loop
  • 0x80031f0c -> psx_lset_world_frame_wrapper
  • 0x80031f6c -> psx_lset_session_teardown
  • 0x800350a8 -> psx_render_mode_dispatch
  • 0x80039ef4 -> psx_level_post_load_runtime_reset
  • 0x80044104 -> psx_present_frame_and_flip

Applied direct technical comments at the same lane anchors:

  • 0x800320bc: load-phase sync point between first runtime blob assignment and detached-blob load path.
  • 0x800391f0: per-type state-script bank assignment path.
  • 0x8003977c: per-type descriptor bank assignment path.
  • 0x80039dc4: runtime-header application into active globals.

Operationally this confirms a stable chain for map storage to frame presentation in this lane:

  1. psx_lset_session_loop drives map-index loads and frame loop control.
  2. lset_level_bundle_load loads runtime blobs and stream payload lanes.
  3. Per-type bank installs (psx_load_type_state_banks and descriptor-bank writes) seed runtime object resources.
  4. Runtime-header and post-load reset apply mission/level state before steady-state updates.
  5. psx_lset_world_frame_wrapper dispatches render mode, world visible draw, and psx_present_frame_and_flip.

Latest Concrete Map 104 0x0042 Pass (2026-04-11)

This batch was anchored to concrete map 104 scene-cache items from scene fingerprint 3497e7f641856415 rather than another broad helper sweep.

Primary sample pack:

  • root lane 0x0030: item:30 / item:31 (raw words: 0042 0b1f 01bf ... 0030)
  • root lane 0x0022: item:25 / item:35 (raw words: 0042 0bff 01a3 ... 0022, 0042 0b1f 01df 0002 ... 0022)
  • constructor lane 0x0030: item:85 / item:86 (raw words: 0042 0b1f/0b3f 019f ... 0030)
  • control constructor sample: item:53 (raw words: 0066 0b13 0183 0000 0001 0020)

Main live symbol recovery in this pass:

  • 0x80017fe8 -> psx_transition_spawn_and_seed_selector_from_record
  • 0x80031044 -> psx_section0_dispatch_root_find_marker_record_by_channel
  • 0x800460fc -> psx_upload_spec_wdl_image_pair_to_vram
  • 0x800463bc -> psx_restore_display_draw_env_after_spec_upload
  • DAT_800675f8 -> psx_type_policy_table_ptr
  • DAT_80063a00 -> psx_type_transition_mode_policy_rows
  • DAT_80063b4c -> psx_type_transition_selector_rows

Family bridge is now explicit

  • psx_dispatch_section0_dispatch_roots is the root-family entry.
  • psx_dispatch_section0_constructor_placements is the constructor-family entry.
  • Both still converge through psx_type_descriptor_table[0x0042] = 0x80063220 -> 0x800626f8.
  • That shared row still enters psx_spawn_compound_record_advance_state_once, then the shared psx_object_create_compound_record / psx_object_advance_state_script path.

This is stronger negative evidence against any remaining idea that root versus constructor family, or 0x019f versus 0x01bf, chooses a different descriptor family for 0x0042. The practical split the viewer still needs remains later in state, flags, nested runtime state, and resource/frame choice.

Spawn-side selector seeding is tighter

psx_transition_spawn_and_seed_selector_from_record now makes the spawn-side authored selector seed explicit before the later type-specific turn/reseat logic.

  • The spawn-side seed feeds psx_object_select_state_from_transition_table.
  • For 0x0042, psx_type42_transition_selector_tick still remains the stronger explanation for live selector 3/4 cases because those low turn selectors can be emitted before the later latch copy.

So exporter/runtime analysis still needs both channels:

  • pre-latch selector evidence
  • later latched obj+0x94-style state

Authored lane to obj+0x1c handoff is now direct

The constructor-side route-flag story is now more concrete than before.

  • Constructors directly copy the authored lane/flags word into obj+0x1c.
  • Runtime logic then mostly mutates low bits such as 0x0002; it is not inventing the initial 0x0022 versus 0x0030 split after the fact.

For the current map 104 sample pack this means:

  • lane 0x0022 starts as authored { 0x0020, 0x0002 }
  • lane 0x0030 starts as authored { 0x0020, 0x0010 }

That is why runtimeDiagnostic.objectLocalRouteFlags.initialWord should be treated as meaningful initial authored state for these records, not only as a later runtime residue.

Nested 0x0400 still reads stronger than object-local 0x0400

This pass still did not recover a new direct object-local obj+0x1c |= 0x0400 writer for 0x0042.

  • The strongest recovered 0x0400 write remains nested-state-side in the wider runtime state machine.
  • 0x0042 continues to read and rewrite nested runtime-state words in the same neighborhood.

So the current best read stays split:

  • object-local obj+0x1c is real authored route/orientation state
  • nested runtime state still carries the strongest recovered 0x0400 stage-selection signal

DAT_800675f8 is tighter as a level-loaded type-policy pointer

DAT_800675f8 is now better modeled as psx_type_policy_table_ptr.

  • It is installed at level load.
  • Runtime readers then index it by type_id << 1 and test the resulting halfword.
  • The currently strongest known masks remain:
    • 0x1000: nearby-interaction publication suppression
    • 0x0600: stage-1 ordering class
    • 0x2000: main-visible semitrans policy

That means runtimeDiagnostic.typePolicy.word should currently be treated as type-global within a loaded level/session, not as a lane-specific candidate. The remaining gap is the live numeric 0x0042 word for the concrete map 104 cases, not the table's general role.

Latest sample-pack follow-up: resource/frame, decode, and routing

The next six-agent concrete pass widened coverage across the same map 104 anchor pack instead of reopening generic theory.

Additional live renames from this pass:

  • 0x80018330 -> psx_transition_selector_probe_nearby_overlap
  • 0x80018414 -> psx_transition_selector_probe_marker_overlap
  • 0x800261f4 -> psx_snapshot_active_object_runtime_rows
  • 0x80025b88 -> psx_release_all_active_objects_and_reset_type_runtime_banks
  • 0x8002f518 -> psx_section0_dispatch_root_seed_marker_channel_table
  • 0x80030ee0 -> psx_section0_dispatch_root_lookup_marker_record_by_kind_channel
  • 0x80030f60 -> psx_section0_dispatch_root_lookup_marker_slot_by_kind_channel
  • 0x80030fcc -> psx_section0_dispatch_root_get_marker_slot_triplet_by_index
  • 0x800311c4 -> psx_section0_dispatch_root_apply_packed_channel_actions
  • 0x80031454 -> psx_section0_dispatch_root_spawn_simple_from_marker_record

Three new practical conclusions matter for the viewer/exporter.

First, the current 0x0042 64x64 versus 64x40 split still looks more like a shared-resource / different-frame-state problem than a distinct pre-draw resource bind.

  • The constructor paths bind type-indexed active-header state before the lane word is copied.
  • Draw-time submitter/frame-geometry consumers still take the live frame token from obj+0x94.
  • So for the current map 104 sample pack, lane 0x0022 versus 0x0030 is still not strong evidence of a different bound resource by itself.

That keeps the strongest next exporter field recommendation the same in spirit but tighter in scope: preserve a stable bound-resource identity separately from the live frame/state token.

Second, the frame-state bridge is now more explicit end to end.

  • psx_object_select_state_script installs selector identity into obj+0x9e, plus the script cursor and intermediate token state.
  • psx_object_advance_state_script later latches the live frame/state token into obj+0x94.
  • Both psx_project_object_main_visible and psx_project_object_special_visible_queue then consume obj+0x94 for frame origin/width/height queries before submission.

This sharpens the current exporter rule again: obj+0x9e and obj+0x94 should not be collapsed into one selector field for 0x0042.

Third, the stage-1 versus stage-2 branch point is now locally explicit even though the exact live 0x0400 provenance for this sample pack is still not closed.

  • psx_object_integrate_motion_and_route_visible tests obj+0x1c & 0x0400 at the decisive branch.
  • For 0x0042, the type-4 special case is irrelevant, so object-local 0x0400 remains the practical stage-2 discriminator.
  • The strongest recovered 0x0400 write is still nested-state-side rather than a direct type-0x0042 object-local OR site.

So the current best routing verdict for the fixed sample pack remains:

  • broadly stage-1/main-visible by default
  • stage-2 only when object-local obj+0x1c actually carries 0x0400 at the routing point

The remaining gap is now very narrow: one live capture pass that samples bound resource identity, obj+0x9e, obj+0x94, obj+0x1c, and the final queue/list path for items 25/30/31/35/85/86.

Focused frame-geometry pass: where 64x40 vs 64x64 is chosen (2026-04-11)

This pass started from the now-stable selector/latch bridge and only followed width/height/origin consumers.

Verified bridge and consumer addresses:

  • 0x800260e8 psx_object_select_state_script: installs selector to obj+0x9e
  • 0x80025d68 psx_object_advance_state_script: latches current frame/state into obj+0x94
  • 0x80040d44 psx_project_object_main_visible: reads obj+0x94, then queries frame origin/width/height from obj+0x10
  • 0x80040f78 psx_project_object_special_visible_queue: same obj+0x94 frame-geometry query path
  • 0x8004513c psx_resource_frame_origin_x, 0x800451d0 psx_resource_frame_origin_y
  • 0x80045014 psx_resource_frame_width, 0x800450a8 psx_resource_frame_height

Decisive evidence from decompile:

  • The projector functions read frame token from obj+0x94 before any frame-geometry call.
  • Width/height/origin helpers branch only on resource kind (4 sprite-header frame records vs 5 image-table frame records) and use the caller-provided frame index.
  • No later projector/draw step rewrites width/height into a 64x40 vs 64x64 family split; later logic applies orientation flip (obj+0x1c & 0x0002) and routing/queue behavior, not a replacement size family.

Conclusion for fixed map 104 0x0042 sample pack:

  • The split is strongest as a live frame-token outcome (obj+0x94) within a shared type resource lane, not as a late presentation modifier.
  • A resource-header-family branch still exists technically (kind 4 vs kind 5 access path), but for same-type 0x0042 objects this is type-level and upstream of per-object frame choice.

Minimum runtime fields to preserve in exporter/viewer for faithful reproduction:

  • obj+0x10 bound resource identity, including resource kind (4 vs 5)
  • obj+0x94 live latched frame/state token (the geometry key)
  • obj+0x9e authored selector seed (kept separate from obj+0x94)
  • obj+0x1c route/orientation flags (0x0002 flip behavior and 0x0400 stage route)

Concrete sample-pack sanity check from current scene cache (map 104, fingerprint 3497e7f641856415):

  • item 25 and 35 currently render 64x40 with route seed 0x0022
  • item 30, 31, 85, 86 currently render 64x64 with route seed 0x0030

This keeps the next unresolved step narrow: capture live bound-resource kind plus live obj+0x94 for those items to close any remaining uncertainty about per-item frame-token divergence.

Final Live Map 104 Cohort Pass (2026-04-12)

Pass scope:

  • active writable SLUS_002.68
  • scene fingerprint 3497e7f641856415
  • fixed anchors:
    • root 0x0022: items 25/35
    • root 0x0030: items 30/31
    • constructor 0x0030: items 85/86
    • control 0x0066: item 53

Functions inspected this pass (create/update/draw + ordering):

  • psx_object_create_simple_record (0x80024b48) - inspected, comment updated
  • psx_object_create_compound_record (0x80025040) - inspected, comment updated
  • psx_object_integrate_motion_and_route_visible (0x800131a8) - inspected, comment updated
  • psx_object_advance_state_script (0x80025d68) - inspected, no edit this pass
  • psx_draw_main_visible_object (0x80041458) - inspected, no edit this pass
  • psx_draw_special_visible_queue (0x80041144) - inspected, no edit this pass
  • psx_main_visible_order_compare_pair_for_graph (0x8002bf0c) - inspected, comment updated

Live artifacts changed in this pass:

  • decompiler comments added/updated:
    • 0x80024b48
    • 0x80025040
    • 0x800131a8
    • 0x8002bf0c

Concrete cohort findings from scene-cache fields and executable behavior:

  1. Differences that are still real across the fixed map-104 cohorts:
    • object-local route seed differs exactly as authored:
      • root 0x0022 (25/35) -> initialWord=0x0022 (bit0x0002=1, bit0x0020=1)
      • root/constructor 0x0030 (30/31/85/86) -> initialWord=0x0030 (bit0x0002=0, bit0x0020=1)
      • control 0x0066 sample (53) -> initialWord=0x0020
    • visible frame geometry differs in exported candidates:
      • 25/35 -> 64x40
      • 30/31/85/86 -> 64x64
      • 53 -> 64x32
  2. Differences that still fail to differentiate the anchor cohorts:
    • stage-route discriminator bit0x0400 remains clear in all sampled anchors (routeOutcomeCandidate=main-visible).
    • pre-latch dispatch and latched-state runtime captures remain unsampled (preLatchDispatchSampled=false, latchedState.sampled=false).
    • per-type policy word remains unsampled (typePolicy.sampled=false, word=null) for both type 0x0042 and control type 0x0066 anchors.
    • per-type art-header kind remains unsampled (resourceKind.sampled=false), so the kind-4 versus kind-5 split is still unresolved at item granularity.

Strongest executable evidence for the next exporter rule:

  • constructors directly preserve authored route/lane word into obj+0x1c (0x80024b48, 0x80025040), so 0x0022 versus 0x0030 is a valid authored split signal.
  • routing to stage-2 still requires type==4 || (obj+0x1c & 0x0400) (0x800131a8), and the fixed anchors currently do not satisfy this.
  • draw submitter path is resource-kind based and shared by both world-visible lanes (0x80041458, 0x80041144); stage-lane choice alone does not pick sprite versus image-table.
  • policy table class bits (0x0600) in 0x8002bf0c are ordering-precedence modifiers, not resource-binding keys.

Practical next exporter rule (highest confidence):

  • Keep map-104 0x0042 cohort partitioning keyed first by authored record_u5/obj+0x1c seed families (0x0022 vs 0x0030) and keep control 0x0066 separate.
  • Do not use typePolicy or stage-route 0x0400 as primary split keys for these anchors until runtime sampling is populated.

Safe immediate renderer/exporter change suggested by this evidence:

  • In scene export/fallback grouping for unresolved map-104 families, add/keep a hard cohort fence:
    • recordRouteFlags.initialWord must match exactly (0x0022 group separate from 0x0030; 0x0066 control separate from 0x0042).
  • Treat typePolicy.word and resourceKind.activeHeaderKindCandidate as optional tie-breakers only when sampled, never as required keys when null.
  • This is safe now because it only prevents cross-cohort over-merge (the repeated-wall failure mode) and does not invent new art mappings.

Evidence Base

This model is grounded by a combination of static and runtime-adjacent evidence:

  • Ghidra decompilation of retail SLUS_002.68
  • the renderer-local .cache scene/export pipeline
  • WDL section parsing and bank extraction work
  • live comparison between exported scene fields and executable object layouts
  • repeated correction of earlier bad hypotheses, especially the disproven "small top-level record stream == whole map" assumption

Most important named functions and data anchors:

  • wdl_resource_bundle_load_by_index
  • psx_object_create_simple_record
  • psx_object_create_compound_record
  • psx_object_select_state_script
  • psx_object_advance_state_script
  • psx_object_lookup_variant_entry
  • psx_object_integrate_motion_and_route_visible
  • psx_object_update_runtime_input_modes
  • psx_project_object_main_visible
  • psx_project_object_special_visible_queue
  • psx_draw_world_visible_passes
  • psx_draw_main_visible_object
  • psx_draw_special_visible_queue
  • psx_main_visible_list_swap_entries
  • psx_main_visible_order_graph_unlink_pair
  • psx_main_visible_order_graph_detach_object
  • psx_level_root_record_stream (DAT_800678f4)
  • psx_section0_dispatch_root_records (DAT_80067720)
  • psx_section0_constructor_placement_records (DAT_800678f0)
  • psx_type_art_template_bank (DAT_800758d8)
  • psx_type_simple_component_bank (DAT_800758d0)
  • psx_type_state_script_bank (DAT_800758cc)
  • psx_type_companion_extents_bank (DAT_800758d4)
  • psx_level_detached_blob (DAT_8006767c)
  • psx_level_decompressed_state_buffer (DAT_8006769c)
  • psx_level_runtime_header_block (DAT_80067794)

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 at DAT_800758d8[type] and materialized drawable resource at DAT_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 call image_bundle_load_to_vram (0x80044614)
  • psx_create_image_resource_from_descriptor (0x80044434) uses the same discriminator:
    • 0x8004445c: kind == 4 branch
    • 0x80044468: kind == 5 branch
    • 0x8004447c: call psx_resource_bind_single_image_vram_slot
    • 0x800444ac: call image_bundle_load_to_vram

Draw-time submitter selection

  • psx_draw_main_visible_object:
    • 0x800415a8: compare resource->kind against 5
    • 0x800415c0: kind == 5 -> psx_image_table_submit_frame (0x80044e9c)
    • 0x800415e0: otherwise -> psx_sprite_resource_submit_frame (0x80044bdc)
  • psx_draw_special_visible_queue:
    • 0x800412c8: compare resource->kind against 5
    • 0x800412dc: kind == 5 -> psx_image_table_submit_frame
    • 0x800412f8: otherwise -> psx_sprite_resource_submit_frame
  • psx_draw_hud_overlay_pass has the same split for slot-managed overlays:
    • 0x80041b5c: image-table submit path
    • 0x80041b84: sprite submit path

Overlay-lane exception

The HUD/overlay lane now has one important exception to the otherwise clean world-object rule.

  • psx_draw_main_visible_object and psx_draw_special_visible_queue branch directly on resource->kind == 5.
  • psx_draw_hud_overlay_pass can route through image-table submission from an overlay-slot flag (0x10) rather than by treating the slot resource like a normal world-object kind check.
  • psx_draw_clock_digits_overlay is a fixed image-table user, not evidence for world-object family binding.

So the viewer/exporter should reuse resource-kind logic for world-object lanes, but it should not mine the HUD/overlay lane as if it were one more map-facing art discriminator.

Runtime implication for viewer binding

  • Treat resource kind == 5 as table-backed UV/frame metadata (psx_image_table_submit_frame) and resource kind != 5 as streamed/decoded sprite uploads (psx_sprite_resource_submit_frame).
  • Keep lane semantics separate:
    • main-visible can OR authored palette high-byte overrides into submit flags
    • special-visible submits without that authored override OR path
    • HUD/overlay has its own slot/table policy and should not be used as a donor for world-object map art binding.

How Map Data Is Stored

1. LSET*.WDL is a multi-section bundle

The executable-backed loader model is no longer speculative:

  • wdl_resource_bundle_load_by_index opens SPEC_A.WDL and the selected LSET*.WDL
  • it reads a 0x38 header whose first nine dwords act as section sizes
  • it then lays out multiple runtime pointers rather than one monolithic map blob

Current map-relevant runtime destinations:

  • psx_level_root_record_stream (DAT_800678f4): top-level root record stream
  • psx_section0_dispatch_root_records (DAT_80067720): secondary 0x18-stride authored record family
  • psx_section0_constructor_placement_records (DAT_800678f0): 0x0c-stride constructor-placement family
  • psx_type_art_template_bank (DAT_800758d8): per-type art/template descriptor bank
  • psx_type_simple_component_bank (DAT_800758d0): per-type simple-record payload bank
  • psx_type_state_script_bank (DAT_800758cc): per-type state-script bank
  • psx_type_companion_extents_bank (DAT_800758d4): per-type variant/companion bank
  • DAT_800675f8: per-type flags table
  • psx_level_detached_blob (DAT_8006767c): additional detached level blob
  • DAT_8006b5d8 -> psx_level_decompressed_state_buffer (DAT_8006769c): optional decompressed 0x3e00 runtime/state substrate
  • psx_level_runtime_header_block (DAT_80067794): separate 0x50 level runtime-header block

The loader/install ownership is now tighter too:

  • psx_load_type_state_banks installs the DAT_800758cc/d0/d4 banks only.
  • psx_stream_install_type_runtime_banks is the packed-stream helper that can install all four per-type banks (DAT_800758cc/d0/d4/d8) together and clears the cached owner/resource slot at DAT_800758c8.
  • psx_snapshot_level_runtime_header_block and psx_apply_level_runtime_header_block make the DAT_80067794 lane read as save or transition state, not as the missing per-type art-binding source for unresolved 0x0042 families.

2. The map is split across authored families, not one row type

The viewer's old region00/region01 labels were a useful stepping stone, but the current executable-backed names are better:

  • section0_dispatch_roots: closest to the DAT_80067720 / root-dispatch family
  • section0_constructor_placements: closest to the DAT_800678f0 constructor-input family

These runtime anchors are now named the same way in the live Ghidra database:

  • section0_dispatch_roots aligns with psx_section0_dispatch_root_records
  • section0_constructor_placements aligns with psx_section0_constructor_placement_records

These families do different jobs.

section0_dispatch_roots:

  • generic runtime-object descriptors
  • fed into per-type dispatch handlers
  • not yet directly final render primitives
  • include families that still need downstream runtime state/variant logic before visible art is known

section0_constructor_placements:

  • tighter constructor-facing rows
  • much closer to direct object spawn inputs
  • already usable for a large part of the viewer export

3. The descriptor-table cluster for 0x003e..0x0050 is shared

The newest Ghidra pass tightens the per-type descriptor story substantially.

  • psx_type_descriptor_table at 0x80063118 is a pointer table.
  • Every currently sampled type in the 0x003e..0x0050 band points to the same descriptor object at 0x800626f8.
  • Type 0x0042 is now pinned exactly too: psx_type_descriptor_table[0x0042] at 0x80063220 points to 0x800626f8.
  • That shared descriptor currently resolves to a small callback cluster that includes:
    • psx_spawn_compound_record_advance_state_once
    • psx_object_refresh_main_visible_and_cleanup
    • psx_object_release_to_free_list
    • psx_spawn_simple_record_set_active_flag
    • psx_object_advance_state_and_queue_special_visible
    • psx_object_create_simple_record
    • psx_object_integrate_motion_and_route_visible

This is important negative evidence. 0x0042 does not currently have a unique descriptor fork that would justify a type-only exporter key. If the viewer needs to distinguish 0x0042 from neighboring generic-family types, that distinction has to be recovered later from runtime-bank content, state progression, flags, lane routing, or resource-kind evidence.

The row layout is now clearer too:

  • descriptor + 0x00: section-0 create/dispatch callback
  • descriptor + 0x04: per-object update callback copied into the live object
  • descriptor + 0x08: release callback
  • descriptor + 0x0c: descriptor flags

The newest pass also tightens the strongest known constructor-placement route for 0x0042 itself:

  • psx_spawn_compound_record_advance_state_once
  • psx_object_create_compound_record
  • psx_object_advance_state_script
  • psx_object_refresh_main_visible_and_cleanup
  • stage-1 main-visible draw through psx_draw_main_visible_object

That is useful negative evidence too. Constructor-placement 0x0042 currently reads as one compound/main-visible path inside the generic descriptor family, not as a special queue or a hidden presentation-only lane.

4. The late template bank matters

The per-type art bank in DAT_800758d8 is not taken from the earlier small-section heuristic.

Current best read:

  • the useful late DAT_800758d8 candidate sits in a late large section
  • it decodes only when that section is treated as a bank with an embedded +0x38 parse start
  • on retail map 9, that correction lifts resolved bundle-mapped items from 0 to 111

This is one of the strongest pieces of evidence that the viewer must respect executable loader structure rather than broad file-wide scans or first-match heuristics.

The art-cache side is narrower now too. psx_type_art_template_bank and psx_type_art_resource_cache_bank no longer read as a simple immutable descriptor-versus-resource split:

  • the cache-build path first seeds psx_type_art_template_bank[type] with the incoming descriptor/header pointer
  • after resource construction, the helper writes the built resource pointer back to both psx_type_art_resource_cache_bank[type] and psx_type_art_template_bank[type]
  • constructor-side consumers then treat psx_type_art_template_bank[type] as the active art header used for kind discrimination, while psx_type_art_resource_cache_bank[type] acts as the reusable per-type built-resource cache

That still does not create a unique 0x0042 branch, but it does mean exporter notes should treat the DAT_800758d8 / DAT_800758c8 pair as active per-type art state, not as one permanently raw descriptor table plus one completely separate cache.

The current live database names now reflect that tighter read too:

  • DAT_800758d8 = psx_type_art_active_header_bank
  • DAT_800758c8 = psx_type_art_built_resource_bank

Constructor Record Layouts

Two constructor families are now strong enough to describe directly.

Compound-record constructor

psx_object_create_compound_record reads:

  • type from record + 0x00
  • x from u16 at +0x02
  • y from u16 at +0x04
  • z from byte +0x06
  • initial state selector from byte +0x08
  • flags from +0x0a

Simple-record constructor

psx_object_create_simple_record reads:

  • type from record + 0x04
  • x from u16 at +0x08
  • y from u16 at +0x0a
  • z from byte +0x0c
  • initial state selector from byte +0x0e

Common constructor outputs

Both constructors:

  • write authored coordinates into object fields +0x3c/+0x40/+0x44 as 16.16 fixed-point
  • preserve the original authored source-record pointer at obj + 0xa0
  • 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_script to seed the initial live state

That preserved source-record pointer at obj + 0xa0 is especially important because it closes the palette-override provenance: later draw code really is reading authored bytes directly.

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 bank
  • psx_type_simple_component_bank (DAT_800758d0): simple-record local payload bank
  • psx_type_state_script_bank (DAT_800758cc): state-script bank
  • psx_type_companion_extents_bank (DAT_800758d4): variant/companion bank

Important object fields

  • obj + 0x10: current drawable resource pointer
  • obj + 0x1c: live route/flags word copied from the authored row and later consumed by visible-routing helpers
  • obj + 0x20..0x2e: projected on-screen rectangle
  • obj + 0x3c/+0x40/+0x44: fixed-point world x/y/z
  • obj + 0x54/+0x58/+0x5c: next/target world position cluster used by motion/integration helpers
  • obj + 0x60/+0x64/+0x68: motion vector used by heading/state reselection helpers
  • obj + 0x78/+0x7c: intermediate projected screen anchor in fixed-point
  • obj + 0x84: current variant bank pointer
  • obj + 0x88: current state-script table pointer
  • obj + 0x8c/+0x90: active script base and current script cursor
  • obj + 0x94: current script word, which is already the live frame/state index used by later draw helpers
  • obj + 0x9e: original authored selector stored by psx_object_select_state_script
  • obj + 0xa0: original authored source-record pointer

The crucial distinction is:

  • obj + 0x9e is the authored input selector
  • obj + 0x94 is the current live script word after advancement/reselection

The executable's later art-facing logic follows obj + 0x94, not obj + 0x9e.

State Scripts, Variants, And Why The Map Is Still Unreadable

1. Initial state selection is not final state selection

psx_object_select_state_script:

  • stores the authored selector at obj + 0x9e
  • chooses an initial script base from DAT_800758cc
  • seeds obj + 0x8c/+0x90

But that is only the starting point.

2. The runtime advances and sometimes reseats the live script

psx_object_advance_state_script:

  • interprets sentinel-driven script records
  • updates obj + 0x94 from the current script word
  • reruns psx_object_lookup_variant_entry

Verified sentinel/control behavior now includes:

  • 0xfffe: psx_script_dispatch_audio_event, a non-visible audio/sequence side-effect dispatch
  • 0xfffd: direct in-family jump
  • 0xfffc: immediate switch to subsidiary script-table entry
  • 0xfffb: scan-forward variant that consumes the next in-band 0xfffd selector before switching

3. Variant lookup is indexed by live state, not by raw placement selector

psx_object_lookup_variant_entry:

  • reads DAT_800758d4
  • indexes it by obj + 0x94
  • does not index directly by obj + 0x9e

That is the key split between authored placement metadata and runtime-visible state.

4. The current DAT_800758d4 evidence points to companion extents, not final art

The newly traced consumer path is narrower and more concrete than the earlier placeholder-art theory.

psx_object_advance_state_script:

  • reruns psx_object_lookup_variant_entry
  • sign-extends the returned three bytes into obj + 0x30/+0x34/+0x38
  • does not update obj + 0x10
  • does not replace the live frame index stored at obj + 0x94

Downstream consumers of obj + 0x30/+0x34/+0x38 are now verified in the interaction lane:

  • psx_object_test_overlap_3d uses those fields as the object's overlap extents against obj + 0x54/+0x58/+0x5c
  • psx_object_update_contact_block_flags uses the same extents while setting directional block/contact bits
  • psx_object_reselect_state_from_target_vector and psx_type4_reselect_motion_state use target-object +0x30/+0x34/+0x38 as target bounds while reseating heading-based state

By contrast, the visible projectors and draw helpers still take visible art only from:

  • the drawable resource pointer at obj + 0x10
  • the live frame/state word at obj + 0x94

So DAT_800758d4 is currently better described as a per-state companion-extents bank than as the last missing direct art table.

5. Interaction and heading state can rewrite the live script

The runtime does not only advance scripts linearly.

Verified reselection path:

  • psx_object_reselect_state_from_target_vector
  • psx_object_quantize_motion_heading16
  • psx_quantize_vector_heading16
  • psx_object_select_state_from_transition_table
  • psx_type42_transition_selector_tick

The live reselection path is now slightly tighter than before:

  • psx_heading16_lookup_unit_vector is the table-backed heading-to-vector helper used by psx_object_reselect_state_from_target_vector when a target-side heading token is available

Verified interaction/reselection cluster:

  • psx_type4_update_delayed_interaction
  • psx_type4_reselect_motion_state
  • psx_object_update_nearby_interactions
  • psx_object_test_overlap_3d
  • psx_object_update_contact_block_flags
  • psx_object_register_contact_pair

Latest live SLUS_002.68 cleanup in this same cluster also closes six previously anonymous helpers that shape post-spawn interaction state:

  • 0x80028050 = psx_object_test_strict_nonoverlap_flag8_pair
  • 0x800281d4 = psx_object_test_strict_nonoverlap_flag8_subject
  • 0x80028700 = psx_object_adjust_param9c_by_view_side
  • 0x800287bc = psx_object_update_param9c_from_contact_target
  • 0x80028eb4 = psx_object_apply_contact_push_bias
  • 0x8002923c = psx_object_spawn_type11_contact_proxy
  • 0x8001ae9c = psx_object_update_interaction_transition
  • 0x8001bca0 = psx_object_select_state_from_transition_table

Current practical read for exporter-facing behavior:

  • +0x30/+0x34/+0x38 remains the shared runtime companion-extents lane used by overlap/reselection checks.
  • +0x9c is actively rewritten after contact/reselection and camera-side scaling, so it should not be treated as spawn-static metadata.
  • contact-triggered type-0x11 proxy spawning now has a direct helper anchor (0x8002923c) inside nearby-interaction flow, not just generic script-advance assumptions.
  • the transition-table lane is now explicit too: psx_object_select_state_from_transition_table uses the per-type table at DAT_80063b4c plus heading-bucket logic to choose selectors before psx_object_select_state_script reseats the active script.
  • psx_object_update_interaction_transition shows one concrete forced-selector path: spawned helper objects are pushed to selector 3 unless they are already in selector 1 or 3.
  • type 0x0042 now has a dedicated transition/update helper too: psx_type42_transition_selector_tick computes and dispatches low turning selectors before the +0x94-style runtime latch copy.
  • for type 0x0042, the DAT_80063b4c row itself mostly yields higher script selectors; the low selectors 3/4 are better explained by that dedicated pre-latch turn/reseat path than by the row alone.
  • the constructor-facing sample path now also closes one adjacent helper: psx_spawn_compound_transition_effect_by_code is a transition-effect constructor helper used in the same 0x0042 transition lane and confirms that selector install writes obj + 0x9e while the later state advance still owns the obj + 0x94 latch.

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_tick uses psx_object_is_within_view_margin as an early gate.
  • it can take a heading bucket from psx_object_compute_heading_selector_to_focus, remap that bucket through mirrored-turn logic, and call psx_object_select_state_script before the latch copy.
  • only after that dispatch does the helper copy its transition result into the +0x94-style runtime latch word.

The newest cache evidence also shows the remaining 0x0042 failure is not one homogeneous placement class. On map 104, the same donor binding currently spans:

  • section0_constructor_placements with u5=0x0020
  • section0_dispatch_roots with a large u5=0x0030 band
  • smaller section0_dispatch_roots outliers with u5=0x0022

In the current cache all of those still land on donor map 85 type 0x0040 bundle 0x0009d304 with palette 0. That is useful as negative evidence: the exporter is currently flattening multiple authored/runtime roles into one wall-like resource before the executable-backed family split is proven.

The viewer now carries a narrow safety rule derived from that evidence. Provisional donor matches in the unresolved generic family are rejected when one map:type bucket spans mixed authored families or mixed raw u5 classes. That does not solve the missing runtime resource rule, but it does stop the exporter from presenting a single false wall field as if the executable had already proven it.

The same six-track verification pass also narrows one part of the u5 split. For root-dispatch 0x0042, the smaller u5=0x0022 cases do not currently read as a separate runtime lane from the dominant u5=0x0030 band. Both still satisfy the same broad main-visible gating through bit 0x0020; the visible difference is that 0x0022 additionally carries bit 0x0002, which affects orientation/extents math and therefore looks more like a presentation variant than a separate queue or subsystem path. So the exporter should keep u5 visible and distinct, but it should not assume every 0x0022 / 0x0030 split implies a different draw pass.

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 raw u5, with decoded 0x0002 / 0x0020 / 0x0200 / 0x0400 bits and a route-outcome candidate
  • selector: raw selector seed plus an explicit note when the type-0x0042 pre-latch turn path may diverge from the later latch
  • latchedState: current exporter-side state/frame candidate used for scene art selection
  • nestedRuntimeState: explicit placeholder slots for the live nested runtime words that still need Ghidra-side sampling
  • resourceKind: per-type active-header/built-resource hints derived from the current art-bank decode
  • typePolicy: explicit placeholder slot for the live DAT_800675f8 word

That payload does not solve the remaining 0x0042 bridge by itself, but it gives the next Ghidra pass a stable schema to fill against concrete scene items instead of re-deriving which channels matter.

Per-Frame World And Render Pipeline

1. Outer lifecycle

  • psx_level_session_loop: outer level-session loop
  • wdl_resource_bundle_load_by_index: level load and runtime-bank setup
  • psx_world_frame_tick: normal per-frame world loop
  • psx_draw_world_visible_passes: top-level draw submission

2. Authored record dispatch before live-object draw

The executable still operates on authored families each frame before or alongside live objects:

  • psx_dispatch_section0_dispatch_roots: dispatches the DAT_80067720 family plus nearby fixed-size entries
  • psx_dispatch_section0_constructor_placements: dispatches the DAT_800678f0 constructor-placement family
  • psx_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 pass
  • psx_run_live_object_behavior_callbacks: later per-object behavior callback pass
  • psx_object_integrate_motion_and_route_visible: integrates motion, updates visibility flags, advances script state, and routes the object to the appropriate render lane

That lane now has two more verified helper clusters that matter for map reconstruction:

  • psx_run_object_behavior_program_tick and psx_object_behavior_opcode_dispatch service a small per-object timed opcode stream before or alongside the main motion/update path
  • psx_world_point_in_view_bounds is the shared world-space cull helper used both by psx_object_integrate_motion_and_route_visible and psx_run_live_object_type_updates
  • psx_object_run_control_opcode, psx_control_move_player_to_point, psx_control_move_object_to_point, psx_control_wait_ticks, psx_control_configure_fixed_camera_anchor, psx_control_set_facing_direction, psx_queue_deferred_control_command, psx_flush_deferred_control_queue, psx_apply_deferred_control_command, psx_apply_deferred_control_to_dispatch_roots, and psx_apply_deferred_control_to_live_objects form a small control-script lane that mutates both per-object motion state and deferred world control state during the world/update step rather than during final draw

The old unnamed post-projection FUN_80027f80 follow-up is now closed too. It is not a hidden render cleanup path. The live helpers are:

  • psx_reset_nearby_interaction_list
  • psx_nearby_interaction_list_add
  • psx_nearby_interaction_list_remove
  • psx_update_motion_and_nearby_interactions

Current best read:

  • psx_object_integrate_motion_and_route_visible and psx_object_refresh_main_visible_and_cleanup enqueue eligible objects into a nearby-interaction active set after projection/state refresh
  • psx_update_motion_and_nearby_interactions consumes that active set before the next frame, running psx_type4_update_delayed_interaction for type 4 objects and psx_object_update_nearby_interactions for the broader collision/contact lane
  • psx_control_move_player_to_point and psx_control_move_object_to_point close control opcode case 1 as a move-to-point instruction rather than a render-side helper, psx_control_wait_ticks closes case 3 as a timed gate on DAT_80078a28, psx_control_configure_fixed_camera_anchor closes cases 4/5 as the fixed-camera-anchor configurator, and psx_control_set_facing_direction closes case 9 as an explicit heading override
  • control opcode case 8 is still not named at the wrapper level, but its direct callee is now grounded: psx_spawn_object_compound_effect_variant3 creates a type-2 compound effect at the current object position, so the wrapper currently reads as a short delay gate around that spawn plus a local motion-state change
  • the separate deferred control-command queue is world-facing control state, not a hidden presentation queue; it is flushed from the per-frame world loop before draw submission and can touch both section-0 dispatch rows and instantiated live objects
  • this queue is therefore part of runtime interaction maintenance, not a separate hidden art-routing pass

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 + 0x1c bit 0x0020 keeps the object in the broad world-visible route class.
  • obj + 0x1c bit 0x0400 selects the stage-2 path: set routes to psx_project_object_special_visible_queue, clear falls through to psx_project_object_main_visible.
  • obj + 0x1c bit 0x0002 still reads as orientation/extents behavior only; it does not switch lanes by itself.
  • DAT_800675f8[type] bit 0x1000 gates the nearby-interaction publish call after projection, and bits 0x0600 feed the stage-1 order-graph comparator.

The writer side is now tighter too:

  • constructors seed obj + 0x1c directly from the authored u5 word
  • psx_object_select_state_from_transition_table can set or clear bit 0x0002
  • the strongest recovered stage-2 consumer remains the route test itself; no equally strong local writer for bit 0x0400 has been confirmed yet in the named helper set, so current best read is still that 0x0400 is usually data-driven from authored state unless one of the remaining anonymous control islands proves otherwise

The anonymous control islands now partially close that last caveat.

  • psx_object_state_machine_dispatch_tick contains a confirmed write of bit 0x0400, but to the nested runtime state word, not directly to the object-local obj + 0x1c halfword.
  • psx_object_handle_control_pair_0a can set global policy bits in DAT_80078a88, clear the nested runtime +0x1c dword, and set object-local bit 0x0200, but this pass still did not recover a direct object-local obj + 0x1c |= 0x0400 write.

So the current best read stays split: 0x0400 is definitely a real stage-selection concept in the wider runtime state machine, but a direct object-local 0x0042 writer is still not pinned in the named helper set.

Stage 1:

  • projector: psx_project_object_main_visible
  • draw helper: psx_draw_main_visible_object
  • object membership stored in psx_main_visible_list (DAT_8006ad5c)

Stage 2:

  • projector: psx_project_object_special_visible_queue
  • draw helper: psx_draw_special_visible_queue
  • queue stored in psx_special_visible_queue (DAT_80078b70) with count psx_special_visible_queue_count (DAT_80067472)
  • reset each frame by psx_reset_special_visible_queue

This is not a HUD-only split. Stage 2 is a real world-facing lane.

The type-flag lane is now more concrete too:

  • DAT_800675f8[type] & 0x1000: suppresses nearby-interaction publication after the stage-2 route point
  • DAT_800675f8[type] & 0x0600: stage-1 ordering priority class used by the order graph and slice sorter
  • DAT_800675f8[type] & 0x2000: main-visible semitrans policy bit at draw time

Those flags now read as ordering, interaction, and draw-policy modifiers. They do not currently overturn the stronger route split recovered from obj + 0x1c.

The type-flag lane is also broader than the first pass suggested, but still policy-only:

  • bit 0x0800: reduced or half-adjust behavior in the radius/param9c decay family
  • bits 0x0010 / 0x0020: target-class selection used by nearby LOS-target search helpers
  • bit 0x0008: flag-8 target-family inclusion gate in contact and decay helpers

These sharpen neighboring gameplay semantics, but they still do not create a unique 0x0042 presentation lane by themselves.

The renamed tail of the top-level draw pass is now clearer too:

  • psx_draw_world_visible_passes ends with psx_draw_hud_overlay_pass (0x800416cc), not a third world-object lane.
  • One concrete child of that pass is psx_draw_clock_digits_overlay (0x8004214c), which formats and submits the timer/clock digits through the image-table submitter.

The adjacent presentation-only helper chain is now named in the live database:

  • psx_level_session_loop calls psx_hud_overlay_init_resources (0x800388a8) during level-session bring-up.
  • psx_hud_overlay_init_resources preloads fixed HUD resources and descriptor defaults used by the later overlay pass.
  • psx_draw_hud_overlay_pass consumes the slot table and calls psx_overlay_slot_step_color_fade (0x80038114) per active slot.
  • overlay/menu controllers allocate and retire those slots through psx_overlay_slot_create (0x80035cc0) and psx_overlay_slot_release (0x80036000).

This chain is a non-map-facing presentation lane: it is layered after both world-visible passes and does not participate in authored map record dispatch, world projection, or stage-1/stage-2 world list routing.

5. Small wrapper helpers prove the split is intentional

Recovered wrappers:

  • psx_spawn_compound_record_advance_state_once
  • psx_spawn_simple_record_set_active_flag
  • psx_object_refresh_main_visible_and_cleanup
  • psx_object_advance_state_and_queue_special_visible

These wrappers show that the executable contains dedicated short-form helpers for:

  • spawn then immediately advance state
  • spawn then mark active
  • refresh and project into stage 1
  • advance state and immediately queue into stage 2

That is strong evidence that route selection is a first-class runtime decision, not an incidental byproduct of the draw code.

Projection Model

The PSX executable-backed coordinate model is now stable enough for viewer use.

World coordinates

Authored world values are stored in object fields:

  • obj + 0x3c: x as 16.16
  • obj + 0x40: y as 16.16
  • obj + 0x44: z as 16.16

Projection

psx_project_object_main_visible and psx_project_object_special_visible_queue use:

  • screen_anchor_x = y - x
  • screen_anchor_y = 2*z - (x + y)/2

They write the fixed-point intermediate anchor to:

  • obj + 0x78
  • obj + 0x7c

Then they subtract camera origin and per-frame origin metrics to produce the final on-screen rectangle at:

  • obj + 0x20..0x2e

This matters for the viewer because PSX sprites should be positioned from those authored screen rectangles, not from a DOS-style reconstructed wireframe.

Render Submission Path

1. Top-level draw pass

psx_draw_world_visible_passes:

  1. asks psx_main_visible_list_get_sorted_slice for the current sorted stage-1 slice
  2. iterates it through psx_draw_main_visible_object
  3. then draws stage 2 through psx_draw_special_visible_queue
  4. then executes the HUD/overlay pass

2. Stage-1 visible-list management

Named helpers:

  • psx_main_visible_list_add
  • psx_main_visible_list_remove
  • psx_main_visible_list_rebucket_object
  • psx_main_visible_list_refresh_from_live_chain
  • psx_main_visible_list_sort_range
  • psx_main_visible_list_get_sorted_slice

The sorter is important because it shows that draw order is not just screen-y or type-only ordering. It uses dependency and tie-break logic tied to the DAT_800915xx ordering data plus depth/position rules.

3. Frame metrics and final resource-specific submitters

Frame metric accessors:

  • psx_resource_frame_origin_x

The final submitter split is now explicit:

  • both psx_draw_main_visible_object and psx_draw_special_visible_queue choose the submitter from the bound resource header at obj + 0x10, not from type id alone
  • if resource->kind == 5, draw goes through psx_image_table_submit_frame
  • otherwise draw goes through psx_sprite_resource_submit_frame

That means image-table-versus-sprite submission is a runtime resource-kind property, not a stable type-family label. For unresolved 0x0042, this is one of the last meaningful missing facts: the decision point is now known exactly, even though the per-map resource-header kind still has to be sampled from runtime-loaded art-bank data.

  • psx_resource_frame_origin_y
  • psx_resource_frame_width
  • psx_resource_frame_height

Resource-specific submitters:

  • psx_sprite_resource_submit_frame
  • psx_image_table_submit_frame

The critical point is that both paths take the live frame index from obj + 0x94.

4. Resource creation path

psx_create_image_resource_from_descriptor:

  • 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_object reads palette overrides from the original source-record pointer at obj + 0xa0
  • for types 0x003e..0x00ab, it uses the high byte of source word +0x06
  • for types >= 0x00ac, it uses the high byte of source word +0x0c

Open:

  • the full rule for all resource classes and all placement families is not yet completely closed
  • the viewer still needs broader CLUT-selection recovery so common cases render with runtime-correct colors without heuristics

So palette work is real, but it is no longer confused with the deeper unresolved art-state bridge.

How To Reassemble A PSX Map In A Viewer

This is the current best practical recipe.

Step 1: parse LSET*.WDL as a multi-section bundle

Do not treat the first small record stream as the whole map.

Required outputs from the loader stage:

  • root/dispatch family payloads
  • constructor-placement family payloads
  • per-type banks for art/state/variant/simple payloads
  • detached extra blob(s)
  • optional decoded DAT_8006769c substrate/state buffer
  • runtime-header block metadata

Step 2: preserve authored rows as authored rows

Keep the exported record families as reversible scene metadata.

Useful naming already in use:

  • section0_dispatch_roots
  • section0_constructor_placements

Do not flatten them into one inferred placement family too early.

Step 3: reconstruct constructor inputs faithfully

For each candidate placement row, reconstruct at least:

  • type
  • authored selector byte
  • authored x/y/z
  • original source bytes needed for later palette and state analysis

Store the original row pointer or offset in exported metadata where practical.

Step 4: resolve per-type runtime banks

For each type, export or reconstruct:

  • art/template descriptor
  • state-script bank
  • companion-extents/variant bank
  • simple-record payload bank when present

This is already partly supported by the current cache/export path through stateLayers and related scene metadata.

Step 5: seed a viewer-side live object model

Minimum object fields for a faithful viewer simulation:

  • resource pointer or resource descriptor reference
  • world x/y/z
  • current script word
  • original selector
  • current companion extents from obj + 0x30/+0x34/+0x38
  • type flags
  • source-record pointer or reconstructed source bytes
  • visible lane classification if known

Step 6: project with the executable's isometric transform

Use:

  • screen_x = y - x
  • screen_y = 2*z - (x + y)/2

Then apply per-frame origin/size offsets and camera subtraction, just as the executable projectors do.

Step 7: separate stage 1 and stage 2

Do not assume one visible-object list.

Viewer-side representation should support:

  • stage-1 main visible list behavior
  • stage-2 queued special-visible path

Even if the first viewer implementation collapses them visually, the underlying scene model should keep them distinct.

Step 8: honor draw-order rules explicitly

The main visible list is sorted, not appended blindly.

A viewer that only sorts by simple y or z will eventually diverge. The current best approximation should be based on the executable-backed visible-list sort behavior, and exported metadata should preserve enough object/dependency information to improve that later.

Step 9: use authored palette bytes where proven

The viewer/exporter should keep both:

  • default resource palette assumptions
  • authored placement override bytes from the preserved source record

That keeps palette work reversible and lets the viewer use stronger executable-backed overrides without hard-wiring them into flattened output.

Step 10: keep the unresolved state-to-art rule explicit

For unresolved families, do not pretend a flat type -> frame table is solved.

Current best practice:

  • use verified executable-backed frame maps only where they are genuinely closed
  • mark unresolved families as provisional
  • preserve the exported state metadata and the DAT_800758d4 companion-extents metadata so later passes can replace placeholders without redoing the whole loader
  • do not treat DAT_800758d4 as a direct fallback art selector unless a family-specific consumer proves otherwise

What A Viewer Can Already Do Reliably

Already defensible from evidence:

  • load PSX maps from the correct LSET*.WDL families
  • separate authored record families instead of flattening them
  • reconstruct multi-level z values for the constructor-placement lane
  • use executable-backed projection math
  • separate stage-1 and stage-2 world lanes in the scene model
  • resolve a first subset of per-type real art from the corrected late DAT_800758d8 bank
  • preserve state banks, companion-extents banks, and decoded runtime blobs as research metadata

What Still Prevents A Fully Readable Map

The remaining blocker is now narrow and concrete:

  • the exact last rule that turns live script/variant state into final visible art for unresolved families

More specifically:

  • the constructors, routing, projection, draw passes, and resource submission path are now substantially understood
  • DAT_800758d4 now looks like per-state companion extents used by overlap/contact logic, not the missing final art table
  • but for unresolved families the viewer still does not fully reproduce how DAT_800758cc, runtime reselection, and family-specific drawable resource/frame presentation interact after the live script changes

That is why the current viewer output is still unreadable as a practical map even though so much of the storage and render machinery is now mapped.

Short-term:

  • keep the current executable-backed export model
  • preserve all state metadata in exported scene JSON
  • export the DAT_800758d4-backed signed companion extents as explicit runtime-bounds metadata instead of treating them as likely art selectors
  • avoid broad fallback art heuristics that overwrite evidence
  • use narrow verified family-specific rules only where backed by executable behavior

Current status of that first export step:

  • the PSX cache builder now decodes DAT_800758d4 as a packed per-state signed extents table
  • exported stateLayers preserve those decoded extents for each type
  • each exported scene item and mapSource row now carries the resolved companionExtents tuple for its chosen live state when available

Medium-term:

  • use the recovered companion extents to improve viewer-side inspection and future occlusion/contact overlays
  • recover the remaining family-specific state-to-art bridge for unresolved root-dispatch families without assuming DAT_800758d4 is the art source
  • close palette selection more broadly once the art-state path is stable enough

Long-term:

  • replace placeholder-heavy families with executable-backed final art selection
  • keep scene export reversible so future corrections do not require a fresh reverse-engineering pass over the raw files

Current Best One-Line Model

The PSX map is stored as a multi-section level bundle plus per-type runtime banks; the executable turns authored rows into live objects, advances and sometimes reseats their state scripts, updates companion extents from DAT_800758d4, projects them into one of two world-facing render lanes, and finally draws resource/frame pairs driven by the live script word. The viewer can now reconstruct most of that chain, and the remaining unreadable output is concentrated in the last unresolved live state-to-art bridge for a few still-placeholder-heavy families.