57 KiB
PSX Map Viewer And JL-9 Investigation Plan
Scope
- Active target: retail PlayStation
SLUS_002.68already loaded in Ghidra. - Keep all PSX documentation in
docs/psx/. - Primary objective: get PSX maps loading into the existing map viewer coherently.
- Secondary objective: make PSX graphics export with the correct palette automatically instead of by partial heuristics.
- Tertiary objective: determine whether
JL-9is a real weapon in the PSX build, how it is unlocked or granted, and which sprite/bundle represents it.
Current State
- The detailed architectural write-up now lives in
docs/psx/map-rendering.md; keep that file as the long-form reference for storage format, runtime banks, render lanes, and viewer reassembly, and keep this file focused on the active plan and remaining blockers. docs/psx/psx.mdalready closes the boot executable, the broadLSET*.WDLlayout, and the likely split between map-like regions and graphics-like regions.- The earlier
region00-firstviewer export is now known to be based on a bad assumption: the~45..59records it exposes per map are only the small top-level WDL descriptor stream, not the full level content. - The stronger current model is a multi-section bundle layout: a top-level
0x18-byte dispatch-record table, typed subordinate resource tables rooted atDAT_800758cc/d0/d4/d8, and at least one separate compressed level-state blob that is inflated intoDAT_8006769cbyFUN_8003b00c(..., 0x3e00, 0x3e00). - The strongest current graphics source remains
post_audio_region_04. - A first PSX debug scene has already been exported experimentally, but the active workflow is now the renderer-local
.cachepipeline rather thansiteoutput. - The active live probe now builds provisional real-art atlases in
map_renderer/src/build-psx-cache.jsfrommap_renderer/STATIC_PSXinto.cache/psx,.cache/reference-data/psx-remorse, and.cache/scene-cache/psx-remorse/.... - The current verified processed build exposes
62PSX maps in the live renderer catalog under the runtime-record scene format (4032atlas-backed shapes,1925packed shared atlases after the latest atlas pass). - The exporter root cause is now clearer: the old five-region post-audio carve was still masking the real visible payload. Loader-sized
post_audio_section_00contains both the small0x18root descriptor rows and the dense 24-byte bulk placement rows, so the cache builder now recovers both visible families from that first real section instead of from the guessedregion00/region01split. - A verified full rebuild now carries
region00 + region01across all62maps.LSET1/L0.WDLnow emits1189items,LSET1/L1.WDLemits754, and every rebuilt map now reportsuniqueZCount > 1instead of the earlier mostly-flatz = 0export. - The next subordinate layers are now structurally split too:
DAT_800758d8is the per-type art/template bank,DAT_800758d0feeds the simple constructor's local component payload, andDAT_800758cc/d4feed the compound constructor's state/variant tables. The renderer-side serialization gap is now closed too: the currentpsx-runtime-record-probe-v6path exports those banks intostateLayers, and the scene writer preserves them in both scene metadata andmapSource. - The live viewer now trims the heavyweight
stateLayersanddecodedRuntimeLayersblobs back out of the interactive runtime scene after load and only reattaches them for scene-JSON export. That keeps the executable-backed research payload reversible without forcing normal pan/zoom interaction to carry the full PSX bank dumps. - The compound-bank finder is broader now too. When a typed-section-16 bank is not found at a parsed section boundary, the cache builder falls back to an absolute-file scan, which is how the late
DAT_800758cc/d0/d4source candidates now land in the exported scene state. - The late LSET template bank is now less speculative too. The currently working map-local
DAT_800758d8candidate is not the old "small typed section" guess; on retailLSET1/L9.WDLit decodes cleanly only when the parser treats the late large section as a bank with an embedded+0x38start, which is now enough to recover real bundle-backed mappings for a first subset of map types. - The main visible bulk layer is no longer flat. The accepted
region01placements now use the constructor-backed+0x06byte as provisionalz, andLSET1/L0.WDLcurrently exports11distinct structured elevation levels instead of one forcedz = 0plane. - One renderer-side mismatch is now closed: PSX sprites use authored
item.screenrectangles, and the bounding/highlight overlay path now uses those same authored rectangles instead of recomputing a DOS-style wireframe from provisionalworldcoordinates. - The executable now closes the last projection stage: authored object coordinates land in object fields
+0x3c/+0x40/+0x44as16.16fixed-point values, andFUN_80040d44/FUN_80040f78project them withscreen_x = y - xandscreen_y = 2*z - (x + y)/2before writing the final screen rectangle at+0x20..+0x2e. - Palette handling is partially grounded by runtime VRAM evidence, but the per-placement override rule is still missing.
- Palette override provenance is now tighter than that older summary:
psx_draw_main_visible_objectonly applies authored override bytes for visible type bands0x003e..0x00aband>= 0x00ac, and it reads them from different source-record words (source+0x06high byte for the earlier band,source+0x0chigh byte for the later band). Types below0x003edo not take that override path in the main visible helper. - The render-lane split matters for color too, not just visibility.
psx_draw_special_visible_queuereuses the same frame/resource submitters as the main visible pass but does not apply the authored palette override byte at all, so any exporter rule that assumes one palette path for all world-facing objects is now too broad. - The submitters themselves also separate the palette story by resource class.
psx_sprite_resource_submit_frameandpsx_image_table_submit_frameboth consume the same high-byte override token, but they translate it through different CLUT lookup tables depending on the bound resource kind, so the remaining viewer bug is not justwhich palette indexbutwhich palette table for which resource class and render lane. - The constructor/resource side is narrower now too. Both
psx_object_create_simple_recordandpsx_object_create_compound_recordresolve the drawable resource at spawn time fromDAT_800758d8before the object enters the live update loop, so the remaining repeated-wall problem is no longer best framed as a missing late resource allocator. The open bridge is the family-specific post-spawn state/frame route plus the correct lane-aware palette policy. - The scene/cache naming now uses executable-backed family names (
section0_dispatch_roots,section0_constructor_placements) with the oldregion00/region01labels kept only as legacy aliases. - The offline
FUN_8003b00cpath now exists in the renderer-local exporter and serializes one candidate on-disk compressed source plus the decoded0x3e00state buffer into the cache for each map. - The type-to-art pass is still open. The exporter now scans parsed per-type template-bank payloads for bundle references, and it no longer promotes the disproven scan-order bundle fallback into visible map art. Unverified types stay on placeholders until the executable state/type path yields a real art binding.
- That loader-shaped bank selection is now already paying off in the live cache: map
9moved from0resolved bundle-mapped items to111after the template pass switched to the embedded late-section parse, even though unresolved root-dispatch families such as0x0042and0x0049still need the downstream state/variant path before they can stop using placeholders. - Current status: the focused PSX exporter is no longer placeholder-driven on
map 104. Placements and projection remain structurally correct, and the current cache now resolves all1002records to actual PSX bundle art, but many of those bindings are still provisional donor selections rather than final executable-proved family/resource closure. - A focused art-binding recovery pass is now landed in the cache builder too; see
docs/psx/art-binding-recovery.mdfor the measured recovery note. The exporter now treats many zero-blockDAT_800758d8constructor-placement types as inherited-art candidates instead of dropping directly to placeholders, first via same-mapDAT_800758ccscript-signature donors and then via a constrained nearest-donor fallback inside the current0x003e..0x0064constructor-placement family band. - That heuristic materially improved the built cache even though it is still provisional rather than executable-proved. The latest rebuild moved the scene set from
58,262fallback items /1,714bundle-mapped items down to25,038fallback items /34,938bundle-mapped items. Representative maps such as0,9, and43are now mostly real-art scenes, whilemap 104remains the clear outlier with866fallback items versus only136bundle-mapped items. - The newest cache slice shows
map 104is failing in a more specific way than “one unresolved type keeps using one bad wall.” The repeated0x0042wall cluster currently spans bothsection0_constructor_placements(u5=0x0020) andsection0_dispatch_roots(u5=0x0030, plus smaller0x0022outliers), yet all of those records still collapse onto donor map85type0x0040bundle0x0009d304with palette0. That means the current donor recovery is over-merging at least two authored/runtime roles before the executable-backed family/resource split is proven. - The viewer/exporter now has a stronger follow-up for that exact failure mode. Provisional donor matches in the unresolved generic family still do not apply as one coarse mixed-role
map:typedefault, but the cache builder now resolves those rows per authored-family plus raw-u5cohort and emits actual PSX art instead of synthetic fallback atlases. Focusedmap 104validation now exports52section0_dispatch_roots-artitems plus950section0_constructor_placements-artitems with0fallback records,1atlas, and136shape definitions. - That no-placeholder pass is intentionally provenance-heavy instead of pretending closure. Scene
mapSourcerows now preservemappingSourceand optionalartCohort, so wrong-art cases can be audited ascohort-*oremergency-global-donor:*matches without reintroducing placeholders into the live renderer. - The latest six-track Ghidra pass narrows that unresolved split further.
0x0042does not have a unique descriptor entry; it shares the same generic descriptor object as the wider0x003e..0x0050band. For constructor-placement0x0042, the strongest current path is now explicit: compound spawn wrapper ->psx_object_create_compound_record->psx_object_advance_state_script-> main-visible refresh/project -> stage-1 draw. For root-dispatch0x0042, the dominantu5=0x0030band and smalleru5=0x0022outliers currently still look like the same broad main-visible runtime role, with0x0002acting more like an orientation/extents variant than a separate lane. - The same pass also closes the submitter side enough to constrain the next art-binding experiment. In both world-facing lanes, draw-time submitter choice is based on the bound resource header kind:
kind == 5usespsx_image_table_submit_frame, otherwise the executable usespsx_sprite_resource_submit_frame. The HUD/overlay lane is now explicitly separated from that rule for viewer purposes: it can route by overlay-slot policy instead of normal world-object kind checks, andpsx_draw_clock_digits_overlayis a fixed image-table user rather than evidence for map-object family binding. - The newest six-track pass tightens the route-state side as well. Constructor and root-dispatch records both hand their authored lane/flags word into
obj+0x1c; current best read is0x0020 = broad world-visible route gate,0x0002 = orientation/extents-axis behavior, and0x0400 = stage-2 special-visible selector. That means the currentu5=0x0030versus0x0022split is still better treated as same-family main-visible routing with different orientation behavior, while the actual stage-1 versus stage-2 split should now be sampled throughobj+0x1cbit0x0400instead of inferred from rawu5alone. - The selector side is narrower now too.
psx_object_select_state_from_transition_table(DAT_80063b4crow indexed bytype - 0x1e) is now a concrete per-type selector source ahead ofpsx_object_select_state_script, andpsx_object_update_interaction_transitionshows one forced-selector path that drives helper objects to selector3unless they are already in1or3. So the next0x0042pass should correlatetype 0x0042transition-table output with the laterobj+0x94live script word instead of treating selector3/4only as a generic post-spawn mystery. - The newest follow-up narrows
0x0042selector recovery again. Type0x0042now has its own transition helper,psx_type42_transition_selector_tick, and that helper can dispatch low turning selectors3/4before the+0x94-style runtime latch copy. The currentDAT_80063b4crow for type0x0042mostly yields higher script selectors, so selector3/4cases now look more like pre-latch turn/reseat outcomes than direct row literals. - The exact descriptor slot is now closed too:
psx_type_descriptor_table[0x0042]at0x80063220points to the shared row0x800626f8, whose create/update/release callbacks arepsx_spawn_compound_record_advance_state_once,psx_object_refresh_main_visible_and_cleanup, andpsx_object_release_to_free_list. Both section-0 constructor placements and root-dispatch rows still enter type0x0042through that same initial descriptor family. - The per-type art/cache lane is narrower too.
DAT_800758d8andDAT_800758c8no longer read as a simple immutable descriptor table plus separate cache; the active helper first seedsDAT_800758d8[type]with the incoming art header, then writes the built resource pointer back to both slots. So the remaining0x0042question is still runtime family/resource selection, but the notes should treat the pair as active per-type art state rather than as a permanently raw-versus-cached split. - The newest follow-up tightens those two lanes again. The live names now reflect the stronger art/cache read:
DAT_800758d8 = psx_type_art_active_header_bankandDAT_800758c8 = psx_type_art_built_resource_bank. On the selector side,psx_type42_transition_selector_ticknow has one more concrete gate: it first checkspsx_object_is_within_view_margin, then can remap a bucket frompsx_object_compute_heading_selector_to_focusand dispatch selector3/4before the later latch copy. So the next runtime sample should include both the pre-latch dispatch state and the laterobj+0x94-style latch, not just one or the other. - The route-bit writer question also narrowed, but is not fully closed yet. The recovered anonymous islands now prove that
0x0400is written in the wider nested runtime state machine (psx_object_state_machine_dispatch_tick) and thatpsx_object_handle_control_pair_0amutates related policy state plus object-local bit0x0200, but this batch still did not pin a direct object-localobj+0x1c |= 0x0400writer for0x0042. That keeps the next concrete sample focused on runtime state plus object-local flags rather than assuming the stage-2 bit is only an authoredu5literal. - Step 2 is now landed on the viewer side as well.
map_renderer/src/lib/psx-cache.jsnow exports a per-itemruntimeDiagnosticpayload in scene versionpsx-runtime-record-probe-v10, carrying the split the last Ghidra passes converged on: object-local route flags, selector seed/pre-latch hints, exporter-side latched-state candidate, nested-runtime placeholders, resource-kind hints, and a placeholder slot for the liveDAT_800675f8policy word. The immediate next Ghidra work should now fill those channels for representativemap 1040x0042items rather than redefining the channel model again. - That first concrete
runtimeDiagnosticpass is now done too.item:25/35(root0x0022),item:30/31(root0x0030),item:85/86(constructor0x0030), and controlitem:53now form the fixedmap 104sample pack for future0x0042checks instead of broad family-level prose. - The family bridge is explicit now:
psx_dispatch_section0_dispatch_rootsandpsx_dispatch_section0_constructor_placementsstill converge through the same shared0x0042descriptor row intopsx_spawn_compound_record_advance_state_once, then the shared constructor/state path. Family identity still matters as an exporter fence, but it is not a different descriptor family. runtimeDiagnostic.objectLocalRouteFlagsis better grounded too. Constructors directly copy the authored lane word intoobj+0x1c, so the exportedinitialWordvalues are real initial authored state. For the current sample pack,0x0022is authored{ 0x0020, 0x0002 }, while0x0030is authored{ 0x0020, 0x0010 }.runtimeDiagnostic.selectoralso stays justified as a separate channel from the later latch. Spawn-side selector seeding now has a named bridge throughpsx_transition_spawn_and_seed_selector_from_record, but0x0042still usespsx_type42_transition_selector_tickto emit selector3/4before the later latch copy.runtimeDiagnostic.nestedRuntimeStateremains necessary too. The strongest recovered0x0400stage-selection write is still nested-state-side, and this pass still did not pin a direct object-localobj+0x1c |= 0x0400writer for0x0042.runtimeDiagnostic.typePolicyis narrower now as well.DAT_800675f8is now explicitlypsx_type_policy_table_ptr, installed during level load and then read by type-indexed consumers, so the eventual numeric policy word should be treated as type-global within the loaded level rather than as a per-lane value.- The next follow-up widened the same concrete sample pack in three practical directions: resource identity, frame-state flow, and explicit routing branches. The strongest current read is still that
map 1040x0042is not yet split by a different descriptor family or an obvious different bind path; the remaining unresolved split is now mostlybound resource identitypluslive frame/state tokenplusobject-local 0x0400 at route time. - The fixed
map 104sample pack should therefore stay frozen for the next pass: root0x0022=item:25/35, root0x0030=item:30/31, constructor0x0030=item:85/86, control0x0066=item:53. - The storage-side section family is tighter now too. The latest pass promotes
DAT_80067938topsx_ctor_placement_section_ptr: a constructor-placement section installed duringwdl_resource_bundle_load_by_indexwhose rows still read as six-halfword0x0c-stride records[type, x, y, z, selector, u5]. That strengthens the current read that map-object construction is fed from explicit section-pack row families rather than from one opaque monolith. - Adjacent section-pack pointers are narrower as well.
DAT_80067838is now at least a real level section base in the same install family rather than an unexplained scalar,DAT_80067840now reads as aresource_bundle_ptr_tablewith control/opcode-stream consumers, andDAT_800676d8now reads aslevel_clut_table, which keeps it on the palette/CLUT side instead of the missing placement/art side. - The decompressed lane is tighter too.
DAT_8006769cis now confirmed again aspsx_level_decompressed_state_buffer, written duringwdl_resource_bundle_load_by_index, passed topsx_lzss_unpack_into_level_buffer, and then reused by save/runtime snapshot code. Current best read is that it primarily seeds runtime-bank installs and broader session/runtime state rather than acting as one hidden flat item table. - The
0x80063e54/0x80063e68/0x800675eccontrol island is now promoted from background curiosity to an active blocker family.0x800675ecis nowpsx_marker_channel_runtime_block, with the strongest current field map at+0x34mode byte,+0x6cmode-step byte, and+0x88/+0x8cshort fields that are still the most plausible presentation-adjacent members. That family still needs an explicit struct/table dump before it can be ruled in or out for map-facing0x0042divergence. - Loader ownership is tighter too:
psx_load_type_state_banksnow reads as theDAT_800758cc/d0/d4installer only,psx_stream_install_type_runtime_banksis the packed-stream helper that can install all four banks includingDAT_800758d8, and theDAT_80067794header block now reads as save/transition state throughpsx_snapshot_level_runtime_header_block/psx_apply_level_runtime_header_block, not as the missing0x0042art-binding source. - The old fallback art binding is now positively disproven for map rendering, not just "still unverified": in the live cache, early
section0_dispatch_rootstypes0x0042and0x0049repeatedly bind to portrait/talk-animation bundles (for example map0offsets0x000B2970and0x000D84F4), which confirms the section-0 dispatch rows are generic runtime-object descriptors whose visible art still depends on downstream per-type state/variant selection. - The executable-side type path is now clearer and named in the live PSX Ghidra database.
psx_object_create_simple_recordandpsx_object_create_compound_recordboth index the same per-type banks rooted atDAT_800758d8/d0/cc/d4;psx_object_select_state_scriptselects an active state script fromDAT_800758cc,psx_object_advance_state_scriptat0x80025d68interprets sentinel-driven script records,psx_object_lookup_variant_entryresolves a companion entry fromDAT_800758d4, andpsx_reset_type_runtime_banks_fromat0x80025ce8is the nearby bank-reset helper that had been misnamed earlier. So the missing map-render rule is not one flattype -> bundletable but a multi-stage runtime selection path. - The visible render pass is less opaque now too.
FUN_80041378draws in three stages: the sorted visible-object list throughFUN_80041458, a second special-visible list throughFUN_80041144, and then HUD/overlay/icon primitives throughFUN_800416cc. That means the remaining map-viewer gap is still mainly in world-object and special-object families, not in the HUD pass. - That draw tail is now named more tightly in Ghidra too:
FUN_800416ccis nowpsx_draw_hud_overlay_pass, and its clock/timer childFUN_8004214cis nowpsx_draw_clock_digits_overlay. - The next decompilation target is narrower now too:
FUN_8002906cis the highest-value follow-up because it is a verified post-construction state reselection path that can overwrite the live script choice fromFUN_8003bc1c(obj) >> 2 & 0xf, which is exactly the missing bridge between exported placement selectors and the art-facingDAT_800758d4variant lookup. - The stage-2 path is now strong enough to affect renderer planning directly.
FUN_80040f78is the queue-builder for theFUN_80041144pass: it projects an object just like the mainFUN_80040d44path but appends it toDAT_80078b70/DAT_80067472instead of the mainDAT_8006ad5cvisible list. So a renderer that only models the stage-1 visible list will still miss a real world-facing object lane. - The broader lifecycle is readable now too.
psx_level_session_loopis the outer level-session loop;wdl_resource_bundle_load_by_indexperforms the actualSPEC_A.WDL+ selectedLSET*.WDLload and root-record dispatch;psx_world_frame_tickis the normal per-frame world loop; andFUN_80041378is the top-level draw submission. - The authored record passes now line up with the viewer model closely enough to use as the current executable ground truth:
psx_dispatch_section0_dispatch_rootsdispatches theDAT_800677200x18-stride family plus the extra fixed-size entries nearDAT_80067658, whilepsx_dispatch_section0_constructor_placementsdispatches theDAT_800678f00x0c-stride family. Those are the closest executable matches for the currentsection0_dispatch_rootsandsection0_constructor_placementsviewer families. - The live-object passes are separated too:
psx_run_live_object_type_updatesruns the per-type update callback over the linked live object list atDAT_800675ac,psx_run_live_object_behavior_callbacksruns the later per-object behavior callback stored on each object, andpsx_update_motion_and_nearby_interactionsis the broad world/player motion integrator that sits between behavior updates and draw submission. - The cull/draw bridge is now explicit too:
psx_authored_record_in_view_boundsgates the two authored record-family dispatch passes,psx_world_point_in_view_boundsgates already-instantiated live objects, andFUN_80041458draws from the final authored screen rectangle while sourcing palette overrides directly from the original record pointer atobj+0xa0. - Palette override provenance is tighter too: object field
+0xa0is the original authored source-record pointer written by both constructors, so the current override path inFUN_80041458is reading authored record bytes directly rather than a hidden runtime side table. - The cache builder now uses that evidence too: shared PSX reference art is keyed by
map:type:palette, and scene export applies the authored palette byte when the source record exposes the proven override lane instead of always baking the bundle default palette. - One narrow renderer-side consequence is now verified in output, not just in notes: the cache builder now applies the executable-backed
0x0050selector map (0..3 -> frame 0..3) as a temporary fallback, and retail map9now exportstype=80 state_selector=1 chosen_frame=1instead of forcing frame0. - The renderer-side state parser is stronger too: the current
buildTypeStateFrameMapspath no longer treats eachDAT_800758ccselector as a flat first-word frame index. It now follows the currently verified sentinel control flow (0xfffe,0xfffd,0xfffc,0xfffb) when building selector-to-frame candidates from the exported state layer. - The top-level level-loader header is now tighter too.
wdl_resource_bundle_load_by_indexreads a0x38file header whose first nine dwords are section sizes, allocates a separate0x50runtime-header block atDAT_80067794, loads one extra non-contiguous blob atDAT_8006767c, and only then optionally inflates theDAT_8006b5d8source intoDAT_8006769c. FUN_80039dc4is now identified as the applier for thatDAT_80067794level runtime-header block, which means the map-viewer export still lacks one executable-backed level metadata lane even after the section-0 and per-type-bank work.- The current best read on that runtime-header lane is narrower now: because the downstream refresh helper is shared with input/menu paths too,
DAT_80067794looks more like per-level runtime mode/camera/presentation state than a hidden extra placement layer. - The state-script control flow is tighter too:
0xfffdis the direct in-family jump control,0xfffcswitches to a subsidiary script table entry immediately, and0xfffbis the scan-forward variant that consumes the next in-band0xfffdselector before switching. - The per-type bank provenance is tighter too.
psx_load_type_state_banksruns twice before the selectedLSET*.WDLopens and twice again after the level-local lanes are loaded, while the standalone late descriptor stream that fillsDAT_800758d8sits between the second pair. SoDAT_800758d8is definitely its own late bank, and the remainingDAT_800758d0/cc/d4lanes belong to the adjacent state-bank blobs rather than to the decompressed0x3e00map-state buffer. - The unresolved type path is more clearly dynamic than before:
psx_object_lookup_variant_entryis called from both constructors and frompsx_object_advance_state_script, so a root-dispatch family can change its visible companion bytes after script jumps instead of fixing them only at spawn time. - The built scene cache now tightens the remaining
0x0042blocker too. The exportedstate_selectorlabel is confirmed to be raw wordu4, not a derived guess, whilelaneis raw wordu5. A full cache scan found3944type=0x0042placeholders across61maps and showed selectors0..4, with real3/4cases onmap-4,map-5,map-8,map-45,map-69, andmap-85. - That same cache scan shows lane and selector are not interchangeable.
0x0042still clusters mostly on lanes0x0020and0x0022, but there are alsolane=0x0030exports withstate_selector=0(for example onmap-108). So the next executable-backed pass should treatu4as the confirmed selector input andu5as a separate lane/class byte while tracing the post-advanceDAT_800758d4variant lookup. - The next narrowing pass is now clearer too: the art-facing variant index is not the raw placement selector byte directly.
psx_object_select_state_scriptstores the authored selector inobj+0x9e, butpsx_object_lookup_variant_entryindexesDAT_800758d4withobj+0x94, the current script word. Two runtime paths (psx_object_reselect_state_from_target_vectorandpsx_type4_reselect_motion_state) can reseat the active script frompsx_object_quantize_motion_heading16(obj) >> 2 & 0xf, so at least some live0x0042selectors3and4come from heading-based runtime reselection rather than from the original record alone. - The newly traced consumer side narrows that further.
psx_object_advance_state_scriptsign-extends theDAT_800758d4lookup result intoobj+0x30/+0x34/+0x38, and the verified downstream consumers of those fields are collision/contact helpers (psx_object_test_overlap_3d,psx_object_update_contact_block_flags, and the target-bounds reads inside the reselection helpers), not the visible draw path. So the practical exporter change is to serializeDAT_800758d4as signed companion-extents metadata and stop treating it as the leading candidate for the missing final art table. - That exporter step is now landed in the viewer-side cache path too.
buildTypeCompanionExtentsMapsdecodes the exportedDAT_800758d4layer as au32 count + packed 3-byte signed extentstable, scenestateLayersnow preserve those decoded extents per type/state, and each exported PSX scene item andmapSourcerow now carries the resolvedcompanionExtentstuple for its chosen live state when one is available. - That moves the remaining art problem to a more specific place: unresolved families still need a family-specific rule that explains how the live script word at
obj+0x94interacts with the drawable resource atobj+0x10after post-spawn reselection. The next useful trace should therefore stay close to those family-specific presentation callers instead of spending more time on genericDAT_800758d4consumers. - The latest dispatch-table pass narrows that again for
0x0042and0x0049: they do not have unique per-type descriptor entries. Both sit inside the same generic descriptor cluster covering0x003e..0x0050, and their shared descriptor currently resolves topsx_spawn_compound_record_advance_state_once,psx_object_refresh_main_visible_and_cleanup, and the newly namedpsx_object_release_to_free_list. So the missing art rule is even less likely to be a special-case table fork and more likely to live downstream in generic object state/resource presentation. - That runtime bridge is now wider and better grounded than it was when
psx_type4_reselect_motion_statefirst became the top target. The newly named cluster around it shows that type-4 delayed interactions can actively rewrite the live script after spawn:psx_type4_update_delayed_interaction(0x80029c20) seeds a forward-probe delay,psx_type4_reselect_motion_state(0x8002906c) either hands off topsx_object_reselect_state_from_target_vectoror recomputes heading from motion state before callingpsx_object_select_state_script,psx_object_update_nearby_interactions(0x80029478) is the broader non-type-4 nearby-object sweep,psx_object_test_overlap_3d(0x80028298) is the box-overlap predicate,psx_object_update_contact_block_flags(0x800289f0) updates directional contact/block bits, andpsx_object_register_contact_pair(0x8002845c) links both objects into the bilateral contact queue. - The motion-heading piece is now named too.
psx_object_reselect_state_from_target_vector(0x80028c94) writes a target-relative motion vector intoobj+0x60/+0x64/+0x68,psx_object_quantize_motion_heading16(0x8003bc1c) wraps the current object motion vector, andpsx_quantize_vector_heading16(0x8003b980) reduces that vector to one of 16 heading buckets before the caller maps it back to aDAT_800758ccscript selector. Combined with the already-verifiedpsx_object_advance_state_script -> psx_object_lookup_variant_entrypath, this means the art-facingDAT_800758d4lookup is driven by the current script word atobj+0x94, not directly by the original placement selector stored atobj+0x9e. - The local render-routing wrappers are named now too, which sharpens the practical exporter story.
psx_spawn_compound_record_advance_state_once(0x80013618) andpsx_spawn_simple_record_set_active_flag(0x8001372c) are constructor-side wrappers that immediately push new objects into specific live states, whilepsx_object_refresh_main_visible_and_cleanup(0x80013688) is a compact stage-1 projector/cleanup wrapper andpsx_object_advance_state_and_queue_special_visible(0x80013758) is a compact stage-2 wrapper that advances script state and immediately queues throughpsx_project_object_special_visible_queue. So the executable already contains small dedicated routing helpers for “advance state, then main visible” versus “advance state, then special visible”, not just one monolithic render path. - The owner-level bridge is named now too:
psx_object_integrate_motion_and_route_visible(0x800131a8) integrates per-object motion, refreshes visibility flags, then advances script state and routes the object into either the stage-1 main visible list or the stage-2 special-visible queue. That means the runtime split relevant to the viewer is not a late draw-only distinction; it is already baked into the per-object motion/update step. - The drawable-resource side is much tighter now too. Both constructors resolve the per-type art bank and store the resulting drawable resource at
obj+0x10before the object enters the live update loop; the current script word atobj+0x94is then passed straight throughpsx_resource_frame_origin_x/y,psx_resource_frame_width/height, and finally intopsx_sprite_resource_submit_frameorpsx_image_table_submit_frameduring the stage-1 and stage-2 draw passes. So the renderer now has a much stronger executable-backed chain from type bank -> object resource pointer -> live frame index -> final primitive submission. - The builder under that constructor-side handoff is named too:
psx_create_image_resource_from_descriptorcreates the drawable resource from the per-type art descriptor, usingimage_resource_bind_vram_slotfor single-image type-4 resources andimage_bundle_load_to_vramfor multi-frame type-5 bundles. That means the remaining viewer gap is no longer “where does the object resource pointer come from”; it is the last live state/variant rule that determines which resource/frame combination unresolved families actually present. - The stage-1 list machinery is named end to end as well:
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, andpsx_main_visible_list_get_sorted_sliceare the concrete helpers behind the main visible-object pass thatpsx_draw_world_visible_passessubmits before the stage-2 queue. - The main-visible ordering rule is no longer safely approximated as a flat
screenYsort.psx_main_visible_list_get_sorted_slicerefreshes only the stage-1 list beforepsx_draw_world_visible_passes, andpsx_main_visible_list_sort_rangethen walks dependency counters, class buckets, and world-extent tie-breakers beforepsx_draw_main_visible_objectsubmits frames. A viewer that only projects coordinates and sorts by one screen axis will still miss part of the executable's coherence model even when art binding is otherwise correct. - The level-load side is part of the same coherence chain too.
psx_level_session_loopcallswdl_resource_bundle_load_by_indexfor the selectedSPEC_A.WDL/LSET*.WDLpair, that load path applieslevel_palette_header_apply, andlevel_palette_header_applyimmediately callslevel_palette_upload_cluts. So the runtime CLUT tables consumed later bypsx_sprite_resource_submit_frameandpsx_image_table_submit_frameare installed during level load before any world-object draw routing begins. - The remaining repeated-wall outlier is now tighter in cache evidence too. On
map 104, the dominant bad cluster is stilltype=0x0042, and the current scene cache repeatedly shows it bindingcross-map-cc-signature-donor:85:0040withtemplate_type=64,donor_type=64,bundle_offset=0x0009d304, andrequested_palette_index=0 resolved_palette_index=0. That means the current visible failure there is not primarily “wrong palette variant picked from a recovered family”; it is still the wrong runtime family/resource presentation rule for that unresolved root-dispatch band. - The same evidence now sharpens the next repair rule too: keep authored family identity and raw
u5class visible until the executable proves a shared resource path.map 104currently shows that matching only on donor template/signature is broad enough to mergesection0_constructor_placementsandsection0_dispatch_rootsinto the same wall art even when their current runtime role is still unproven. - Practical consequence for the viewer plan: the exported placement selector is now firmly only an input, not a final art choice. The immediate placeholder problem is removed for the focused exporter path, but the next bridge still must model post-spawn interaction/reselection well enough to replace
cohort-*andemergency-global-donor:*provenance with tighter executable-backed family/resource/frame rules. JL-9already appears in recovered PSX weapon-name tables, but gameplay availability and sprite identity are not yet closed.
Success Criteria
Map-viewer success
- At least one PSX map loads in the existing viewer with stable world placement, defensible draw order, and recognizable room/layout structure.
- The PSX path reuses the existing viewer pipeline instead of creating a separate one-off viewer.
- Exported scene data preserves enough raw metadata to keep later decomp passes reversible.
Palette success
- Bundle export chooses the same palette family the runtime uses for that placement class.
- At least one tile-heavy scene and one object-heavy scene render with mostly correct colors without manual palette swapping.
- Palette selection logic is encoded in exporter metadata or viewer-side decode rules, not only in prose notes.
JL-9 success
JL-9is classified as one of: fully usable weapon, cut/incomplete leftover, menu-only string, or debug-only grant.- The unlock or acquisition path is identified from executable logic, data tables, or authored content.
- The weapon's sprite or best candidate art bundle is identified and documented.
Workstreams
1. Close the PSX map record format
Purpose: replace the invalid small top-level record stream == whole level assumption with a renderer-fed scene that includes the real bulk map substrate.
Tasks:
- Revisit the executable loader chain around the
LSET*.WDLstream consumer and name the section families loaded intoDAT_800678f4,DAT_80067720,DAT_800758cc/d0/d4/d8,DAT_800675f8, andDAT_8006769c. - Prove which loaded section is the small top-level object/dispatch list and which section holds the actual bulk map substrate.
- Recover the format and semantics of the compressed blob that
FUN_8003b00cinflates into the0x3e00level buffer. - Tie one concrete subordinate record family to the constructor inputs that feed object
+0x3c/+0x40/+0x44as16.16fixed-point coordinates. - Recover the bundle/frame binding rule for map placements well enough to stop relying on broad candidate pairing.
- Recover the draw-order or layer rule used when multiple map records overlap.
- Validate the corrected multi-section schema on at least
L0.WDLandL1.WDLso the decode is not overfit to one level. - Dump the installed section-pack/runtime tables after level load for
map 104, especiallypsx_ctor_placement_section_ptr,psx_type_policy_table_ptr,level_clut_table, and the0x80063e54/0x80063e68/psx_marker_channel_runtime_blockfamily. - Correlate those live table rows against the fixed
map 1040x0042sample pack (item:25/30/31/35/85/86) before widening any donor or family heuristics again.
Expected output:
- a stable PSX placement schema recorded in
docs/psx/ - one exporter that emits scene JSON in the same broad shape as the existing viewer pipeline
- one known-good reference map whose structure is visually recognizable
2. Close palette selection instead of guessing it
Purpose: make exported graphics match the runtime palette path automatically.
Tasks:
- Continue from the already identified texture draw helpers and the caller path that reads palette override metadata from the object field currently described as
+0xa0in the notes. - Determine whether the placement record itself, a second-stage runtime header, or a side table supplies the override palette index.
- Reconcile the live VRAM
row 0xF0 / x=0success case against the on-disk palette blob so the export path can reproduce the runtime source instead of only matching dumps. - Identify whether different bundle modes or resource classes use different CLUT selection rules.
- Add exporter-side palette metadata that preserves both bundle default palette and resolved placement palette.
- Validate against at least three anchor assets: one wall/floor-heavy tile set, one object sprite with obvious color identity, and one UI or portrait-like asset.
Expected output:
- a documented palette-selection rule in
docs/psx/ - exported PSX atlases or frame PNGs that no longer require manual palette picking for the common solved families
- a short unresolved list only for genuinely exceptional palette cases
3. Integrate the PSX decode into the existing map viewer
Purpose: stop treating PSX as a disconnected experiment and make it a first-class renderer source.
Tasks:
- Define one PSX scene format version that keeps raw decode fields visible while still fitting the current viewer's atlas-plus-scene model.
- Export one minimal but real PSX map scene from the solved map schema and load it through the existing viewer path.
- Compare the rendered result against in-game screenshots, captured VRAM/framebuffer evidence, or clearly identifiable room geometry.
- Tighten the exporter until one map reads coherently before trying to bulk-export the entire disc.
- Only after a coherent single-map success, generalize to more
LSETmaps and add any PSX-specific catalog or loader toggles the viewer needs.
Expected output:
- one coherent PSX map visible in the existing viewer
- one stable exporter path that can be iterated on without forking the viewer architecture
4. Investigate JL-9 as data, logic, and art
Purpose: close the question of whether JL-9 is real and what it corresponds to visually.
Tasks:
- Locate the PSX weapon-name table and the code/data structure that indexes into it.
- Identify the item or weapon definition row for
JL-9, including ammo type, flags, and any inventory/equipability markers. - Trace all code and data references to that row: mission rewards, cheats, debug grants, pickups, shop/loadout flow, or scripted usecode equivalents if present.
- Check whether
JL-9appears in the pre-alpha build under the same index and whether its surrounding data differs from retail. - Identify the sprite by following the weapon/item definition to the bundle/frame or icon resource it uses.
- Classify the result clearly: shipped and obtainable, shipped but gated/unused, or string/data leftover only.
Expected output:
- a short
docs/psx/note or section that states whetherJL-9is real - the acquisition or unlock path if one exists
- the best supported sprite or bundle match
Recommended Execution Order
- Finish map-record closure enough to bind placements to the right art.
- Replace the current
.cacheruntime-record probe premise with the corrected multi-section WDL model, then recover the runtime type/resource lookup that can replace the still-provisionalu0 -> bundle indexrule with real art binding. - Get one map loading coherently in the existing viewer.
- After the viewer path is grounded, use the now-stronger bundle identification flow to close
JL-9sprite identity and availability.
Immediate Next Batch
- In Ghidra, tighten the section-family naming around
DAT_800678f4,DAT_80067720, and the candidateDAT_8006b5d8source so the currentsection0_*labels can be promoted from exporter-safe names to exact loader names. - Record which helpers read
DAT_80067720, which apply the separateDAT_80067794runtime-header block, and which helpers read the decompressedDAT_8006769cbuffer now that the offline decode path is present in the cache. - Compare the rebuilt all-map exports against recognizable rooms and decide whether the remaining missing structure now lives mainly in the decoded
DAT_8006769cbuffer or in still-unrendered subordinate tables. - Tighten the raw file mappings for the newly exported runtime-bank layers (
DAT_800758d8,DAT_800758d0,DAT_800758cc,DAT_800758d4) so their current section selection is proven rather than heuristic. Current delta: the bank split is stronger now.DAT_800758d8comes from its own late descriptor stream, whileDAT_800758d0/cc/d4belong to the adjacentpsx_load_type_state_banksblobs. The remaining open question is the exact sub-split inside those state-bank blobs, not whether they come from the decompressed level-state lane. - Split the remaining art/palette problem by render lane and resource class before widening donor logic again. For each repeated-wall family, determine whether it reaches
psx_draw_main_visible_objectorpsx_draw_special_visible_queue, whether its bound resource atobj+0x10is sprite-kind or image-table-kind, and whether the authored override byte should be interpreted through the main visible override path at all. Current delta:psx_draw_main_visible_objectnow proves a banded override rule (0x003e..0x00ab -> source+0x06 high byte,>=0x00ac -> source+0x0c high byte), whilepsx_draw_special_visible_queueskips that authored override entirely. Current delta:psx_sprite_resource_submit_frameandpsx_image_table_submit_frameboth accept the same high-byte override token but resolve it through different CLUT tables, so the exporter must stop treating palette selection as a type-only scalar. Current delta: the constructors already bind the drawable resource fromDAT_800758d8before any later state transitions, so the remaining aliasing is not best attacked as a missing late bundle lookup. Current delta: the worst visible outlier onmap 104is stilltype=0x0042repeatedly mapped to donor map85type0x0040bundle0x0009d304with palette0, so the next repair pass should treat it first as a wrong family/resource binding problem, not as a missing alternative palette decode. Current delta: the current donor pass is broad enough to mergesection0_constructor_placements u5=0x0020andsection0_dispatch_roots u5=0x0030/0x0022onto that same donor wall, so the next exporter-safe experiment should keep family plus rawu5as a hard fence before broadening any cross-map donor reuse further. Current delta: that hard fence is now implemented in the viewer cache builder for provisional generic-family donor matches. The cache now prefers honest placeholders over one shared wrong wall when a single type bucket mixes authored families oru5classes. Current delta: the new subagent sweep says the next split should not be “invent a new per-type descriptor bucket for0x0042,” because0x0042still shares the generic0x003e..0x0050descriptor cluster. The next executable-backed discriminator should be runtime-bank content, state progression, resource kind, and lane/flag behavior instead. - Recover an actual family-specific state-to-frame rule from the post-spawn runtime path so the exporter can replace the still-provisional
state_selector -> chosen_framefallback with the liveobj+0x94path used by the submitters. Current delta: the unresolved families are clearly dynamic becausepsx_object_lookup_variant_entryreruns afterpsx_object_advance_state_script,psx_object_reselect_state_from_target_vector, andpsx_type4_reselect_motion_state; the next verified art-binding pass should therefore sample post-jump state, not only constructor-time selectors. Current delta: the executable's coherent-map path is now ordered as load-time CLUT install -> constructor-side resource bind -> per-frame motion/state advance -> stage-1 or stage-2 visibility routing -> stage-1 dependency sort -> frame submission. Any next exporter shortcut should be checked against that order before being treated as a stable map rule. Current delta: draw-time submitter choice is now explicit too.psx_draw_main_visible_objectandpsx_draw_special_visible_queueboth choosepsx_image_table_submit_frameonly when the bound resource header kind is5; otherwise they usepsx_sprite_resource_submit_frame. So unresolved0x0042still needs runtime resource-kind evidence, not just lane or type labels. Current delta: treat the HUD/overlay lane as a false lead for this specific repair.psx_draw_hud_overlay_passcan route through slot flags rather than the normal world-object kind split, andpsx_draw_clock_digits_overlayis forced image-table presentation. Neither should be used to infer0x0042world-object art binding. Current delta: the next executable-backed sample should be concrete, not another broad sweep. Formap 104, trace at least one constructor-placementtype=0x0042 u5=0x0020case and one root-dispatchtype=0x0042 u5=0x0030case throughobj+0x10resource kind,obj+0x94live script word, and final world-facing lane so the exporter can decide whether these are truly separate presentation families. Current delta: the concrete sample pack is now fixed. Useitem:25/35as the root0x0022control,item:30/31as the root0x0030control, anditem:85/86as the constructor0x0030control before widening any art-binding experiment. Current delta: includeobj+0x1cin that concrete sample. The next trace should explicitly record whether bit0x0400is ever set for representativemap 1040x0042objects, because that is now the strongest recovered stage-2 selector and is more decisive than rawu5by itself. Current delta: include the per-type transition-table row in that same sample. Fortype 0x0042, inspect theDAT_80063b4crow used bypsx_object_select_state_from_transition_tableand compare its selector outputs against the observedobj+0x9e/obj+0x94values; that is now the clearest route to explaining selector3/4cases without reopening broad donor heuristics. Current delta: do not stop at the latchedobj+0x94value. For representative0x0042cases, sample the pre-latch selector dispatch path throughpsx_type42_transition_selector_tickas well; selector3/4can be selected there before the runtime latch copy, so a pureobj+0x94snapshot can still miss the active turn-state choice. Current delta: treatDAT_800675f8as policy, not as the main route discriminator.0x1000is now best read as nearby-publication suppression after stage-2 routing,0x0600as stage-1 ordering class, and0x2000as main-visible semitrans policy. The next0x0042split still belongs first toobj+0x1c, pre-latch selector behavior, and bound resource kind. Current delta: include the early-gate side in that same sample.psx_type42_transition_selector_ticknow clearly checkspsx_object_is_within_view_marginbefore the pre-latch turn/reseat dispatch, so the next runtime sample should log whether the object is even eligible to emit the transient selector before trying to explain a final frame choice. Current delta: keep the0x0400question scoped correctly. The latest anonymous-island recovery proves wider runtime-state writes and related policy control, but not yet a direct object-localobj+0x1c |= 0x0400writer. So the next pass should explicitly separate object-localobj+0x1c, nested runtime state words, and global mode bits instead of folding them into one generic “route flag” bucket. Current delta: treatruntimeDiagnostic.typePolicy.wordas a level-global per-type value when you do capture it. The latest pass now closes the table role more tightly than before:DAT_800675f8is a level-loaded type-policy pointer, not a per-lane word source. Current delta: use the new cache export as the comparison surface. The next batch should target concretemapSource.items[*].runtimeDiagnosticchannels in the generated PSX scene JSON and tie live Ghidra evidence back to those exact fields, especially for representativemap 1040x0042placeholders. Current delta: the next exporter/runtime correlation should add a stable bound-resource identity plus the live frame/state token used at submission if the current fields are not enough. The concrete0x0042sample pack now strongly suggests that0x0022versus0x0030is not itself the final64x40versus64x64split. Current delta: the route branch itself is no longer vague.psx_object_integrate_motion_and_route_visiblenow has an explicitobj+0x1c & 0x0400stage-2 branch point commented in the live database, so the next runtime capture does not need to hunt for the branch location, only to determine whether the concrete sample items ever carry that bit at the decisive moment. - Split section-0 placements into at least four executable-backed presentation classes: main-visible objects with authored palette override, special-visible objects without that override, animated runtime-only objects, and clearly non-map-facing UI/talk assets such as the portrait bundles currently surfacing through fallback art matching.
- Decode the
psx_object_advance_state_scriptsentinel opcodes (ffff,fffe,fffd,fffc,fffb) well enough to tell when a placement loops, jumps into a subsidiary script, or fires a side-effect helper, because that state-machine branch is now the main discriminator between map-facing art and non-map runtime assets. Current delta:fffeis closed as an audio/effect dispatch throughpsx_script_dispatch_audio_event,fffdis the direct indexed jump, andfffc/fffbare now separated as the immediate subsidiary-switch versus scan-forward subsidiary-switch pair. - Recheck the renderer-side palette export against those executable rules before another broad cache rebuild. In particular, verify that map-scene palette variants only use authored override bytes for the type bands and render lanes where
psx_draw_main_visible_objectactually consumes them, and keep special-visible families on their resource-default path until a stronger rule is proved.
Current delta: live function coverage is now measurable instead of anecdotal. The active SLUS_002.68 session currently has 1274 local 0x800... functions, 917 named and 357 still anonymous, which puts the practical local naming floor at 71.98% and leaves 28.02% still unresolved. The hottest remaining anonymous pages are currently 0x8002exxx, 0x80030xxx, and 0x80049xxx with 21 unknowns each.
Current delta: one adjacent world/update family is now tighter too. The 0x80023c..2b1.. lane is no longer just a pile of anonymous helpers: psx_object_run_control_opcode now owns the local control-stream switch, case 1 is split into psx_control_move_player_to_point and psx_control_move_object_to_point, case 3 is psx_control_wait_ticks, cases 4/5 are now closed as psx_control_configure_fixed_camera_anchor, case 9 is psx_control_set_facing_direction, and case 2 is grounded as a deferred control-command queue (psx_queue_deferred_control_command -> psx_flush_deferred_control_queue -> psx_apply_deferred_control_command -> dispatch-root/live-object appliers). Case 8 is still not named at the wrapper level, but its direct callee is now closed as psx_spawn_object_compound_effect_variant3, which narrows the remaining uncertainty to the wrapper's timing and motion-side effects rather than the spawned effect itself.
8. In parallel with the map pass, trace the palette-override read path from the known draw helper caller and document which source field feeds the resolved CLUT.
9. Locate the JL-9 weapon entry in the PSX executable tables and log its table index, surrounding weapon names, and all code/data xrefs.
10. Create a short follow-up note in docs/psx/ after the batch rather than burying the result only in Ghidra comments.
Documentation Rule For This Track
- Keep long-form findings in
docs/psx/psx.mdor another dedicated file underdocs/psx/. - Keep this file as the active plan and update it when a major blocker closes or the execution order changes.
- When
JL-9closes cleanly, give it its own short note underdocs/psx/instead of leaving it as one bullet in a larger map note.