Crusader_Decomp/docs/psx/map-viewer-plan.md
2026-04-07 17:16:44 +02:00

200 lines
No EOL
32 KiB
Markdown

# PSX Map Viewer And JL-9 Investigation Plan
## Scope
- Active target: retail PlayStation `SLUS_002.68` already 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-9` is 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.md` already closes the boot executable, the broad `LSET*.WDL` layout, and the likely split between map-like regions and graphics-like regions.
- The earlier `region00-first` viewer export is now known to be based on a bad assumption: the `~45..59` records 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 at `DAT_800758cc/d0/d4/d8`, and at least one separate compressed level-state blob that is inflated into `DAT_8006769c` by `FUN_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 `.cache` pipeline rather than `site` output.
- The active live probe now builds provisional real-art atlases in `map_renderer/src/build-psx-cache.js` from `map_renderer/STATIC_PSX` into `.cache/psx`, `.cache/reference-data/psx-remorse`, and `.cache/scene-cache/psx-remorse/...`.
- The current verified processed build exposes `62` PSX maps in the live renderer catalog under the runtime-record scene format (`4032` atlas-backed shapes, `1925` packed 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_00` contains both the small `0x18` root 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 guessed `region00/region01` split.
- A verified full rebuild now carries `region00 + region01` across all `62` maps. `LSET1/L0.WDL` now emits `1189` items, `LSET1/L1.WDL` emits `754`, and every rebuilt map now reports `uniqueZCount > 1` instead of the earlier mostly-flat `z = 0` export.
- The next subordinate layers are now structurally split too: `DAT_800758d8` is the per-type art/template bank, `DAT_800758d0` feeds the simple constructor's local component payload, and `DAT_800758cc/d4` feed the compound constructor's state/variant tables. The renderer-side serialization gap is now closed too: the current `psx-runtime-record-probe-v6` path exports those banks into `stateLayers`, and the scene writer preserves them in both scene metadata and `mapSource`.
- The live viewer now trims the heavyweight `stateLayers` and `decodedRuntimeLayers` blobs 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/d4` source candidates now land in the exported scene state.
- The late LSET template bank is now less speculative too. The currently working map-local `DAT_800758d8` candidate is not the old "small typed section" guess; on retail `LSET1/L9.WDL` it decodes cleanly only when the parser treats the late large section as a bank with an embedded `+0x38` start, 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 `region01` placements now use the constructor-backed `+0x06` byte as provisional `z`, and `LSET1/L0.WDL` currently exports `11` distinct structured elevation levels instead of one forced `z = 0` plane.
- One renderer-side mismatch is now closed: PSX sprites use authored `item.screen` rectangles, and the bounding/highlight overlay path now uses those same authored rectangles instead of recomputing a DOS-style wireframe from provisional `world` coordinates.
- The executable now closes the last projection stage: authored object coordinates land in object fields `+0x3c/+0x40/+0x44` as `16.16` fixed-point values, and `FUN_80040d44` / `FUN_80040f78` project them with `screen_x = y - x` and `screen_y = 2*z - (x + y)/2` before 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.
- The scene/cache naming now uses executable-backed family names (`section0_dispatch_roots`, `section0_constructor_placements`) with the old `region00/region01` labels kept only as legacy aliases.
- The offline `FUN_8003b00c` path now exists in the renderer-local exporter and serializes one candidate on-disk compressed source plus the decoded `0x3e00` state 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 `9` moved from `0` resolved bundle-mapped items to `111` after the template pass switched to the embedded late-section parse, even though unresolved root-dispatch families such as `0x0042` and `0x0049` still need the downstream state/variant path before they can stop using placeholders.
- Current status: the PSX viewer output is still unreadable as a practical map. The exported placements and projection are good enough to sketch room massing, but most visible section-0 families still lack the executable's final state/variant-driven art binding and therefore remain on placeholders.
- A focused art-binding recovery pass is now landed in the cache builder too; see `docs/psx/art-binding-recovery.md` for the measured recovery note. The exporter now treats many zero-block `DAT_800758d8` constructor-placement types as inherited-art candidates instead of dropping directly to placeholders, first via same-map `DAT_800758cc` script-signature donors and then via a constrained nearest-donor fallback inside the current `0x003e..0x0064` constructor-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,262` fallback items / `1,714` bundle-mapped items down to `25,038` fallback items / `34,938` bundle-mapped items. Representative maps such as `0`, `9`, and `43` are now mostly real-art scenes, while `map 104` remains the clear outlier with `866` fallback items versus only `136` bundle-mapped items.
- The old fallback art binding is now positively disproven for map rendering, not just "still unverified": in the live cache, early `section0_dispatch_roots` types `0x0042` and `0x0049` repeatedly bind to portrait/talk-animation bundles (for example map `0` offsets `0x000B2970` and `0x000D84F4`), 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_record` and `psx_object_create_compound_record` both index the same per-type banks rooted at `DAT_800758d8/d0/cc/d4`; `psx_object_select_state_script` selects an active state script from `DAT_800758cc`, `psx_object_advance_state_script` at `0x80025d68` interprets sentinel-driven script records, `psx_object_lookup_variant_entry` resolves a companion entry from `DAT_800758d4`, and `psx_reset_type_runtime_banks_from` at `0x80025ce8` is the nearby bank-reset helper that had been misnamed earlier. So the missing map-render rule is not one flat `type -> bundle` table but a multi-stage runtime selection path.
- The visible render pass is less opaque now too. `FUN_80041378` draws in three stages: the sorted visible-object list through `FUN_80041458`, a second special-visible list through `FUN_80041144`, and then HUD/overlay/icon primitives through `FUN_800416cc`. That means the remaining map-viewer gap is still mainly in world-object and special-object families, not in the HUD pass.
- The next decompilation target is narrower now too: `FUN_8002906c` is the highest-value follow-up because it is a verified post-construction state reselection path that can overwrite the live script choice from `FUN_8003bc1c(obj) >> 2 & 0xf`, which is exactly the missing bridge between exported placement selectors and the art-facing `DAT_800758d4` variant lookup.
- The stage-2 path is now strong enough to affect renderer planning directly. `FUN_80040f78` is the queue-builder for the `FUN_80041144` pass: it projects an object just like the main `FUN_80040d44` path but appends it to `DAT_80078b70` / `DAT_80067472` instead of the main `DAT_8006ad5c` visible 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_loop` is the outer level-session loop; `wdl_resource_bundle_load_by_index` performs the actual `SPEC_A.WDL` + selected `LSET*.WDL` load and root-record dispatch; `psx_world_frame_tick` is the normal per-frame world loop; and `FUN_80041378` is 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_roots` dispatches the `DAT_80067720` `0x18`-stride family plus the extra fixed-size entries near `DAT_80067658`, while `psx_dispatch_section0_constructor_placements` dispatches the `DAT_800678f0` `0x0c`-stride family. Those are the closest executable matches for the current `section0_dispatch_roots` and `section0_constructor_placements` viewer families.
- The live-object passes are separated too: `psx_run_live_object_type_updates` runs the per-type update callback over the linked live object list at `DAT_800675ac`, `psx_run_live_object_behavior_callbacks` runs the later per-object behavior callback stored on each object, and `FUN_80029de0` is the broad world/player motion integrator that sits between behavior updates and draw submission.
- The cull/draw bridge is now explicit too: `FUN_800423b0` gates the two authored record-family dispatch passes, `FUN_80042424` gates already-instantiated live objects, and `FUN_80041458` draws from the final authored screen rectangle while sourcing palette overrides directly from the original record pointer at `obj+0xa0`.
- Palette override provenance is tighter too: object field `+0xa0` is the original authored source-record pointer written by both constructors, so the current override path in `FUN_80041458` is 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 `0x0050` selector map (`0..3 -> frame 0..3`) as a temporary fallback, and retail map `9` now exports `type=80 state_selector=1 chosen_frame=1` instead of forcing frame `0`.
- The renderer-side state parser is stronger too: the current `buildTypeStateFrameMaps` path no longer treats each `DAT_800758cc` selector 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_index` reads a `0x38` file header whose first nine dwords are section sizes, allocates a separate `0x50` runtime-header block at `DAT_80067794`, loads one extra non-contiguous blob at `DAT_8006767c`, and only then optionally inflates the `DAT_8006b5d8` source into `DAT_8006769c`.
- `FUN_80039dc4` is now identified as the applier for that `DAT_80067794` level 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_80067794` looks more like per-level runtime mode/camera/presentation state than a hidden extra placement layer.
- The state-script control flow is tighter too: `0xfffd` is the direct in-family jump control, `0xfffc` switches to a subsidiary script table entry immediately, and `0xfffb` is the scan-forward variant that consumes the next in-band `0xfffd` selector before switching.
- The per-type bank provenance is tighter too. `psx_load_type_state_banks` runs twice before the selected `LSET*.WDL` opens and twice again after the level-local lanes are loaded, while the standalone late descriptor stream that fills `DAT_800758d8` sits between the second pair. So `DAT_800758d8` is definitely its own late bank, and the remaining `DAT_800758d0/cc/d4` lanes belong to the adjacent state-bank blobs rather than to the decompressed `0x3e00` map-state buffer.
- The unresolved type path is more clearly dynamic than before: `psx_object_lookup_variant_entry` is called from both constructors and from `psx_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 `0x0042` blocker too. The exported `state_selector` label is confirmed to be raw word `u4`, not a derived guess, while `lane` is raw word `u5`. A full cache scan found `3944` `type=0x0042` placeholders across `61` maps and showed selectors `0..4`, with real `3`/`4` cases on `map-4`, `map-5`, `map-8`, `map-45`, `map-69`, and `map-85`.
- That same cache scan shows lane and selector are not interchangeable. `0x0042` still clusters mostly on lanes `0x0020` and `0x0022`, but there are also `lane=0x0030` exports with `state_selector=0` (for example on `map-108`). So the next executable-backed pass should treat `u4` as the confirmed selector input and `u5` as a separate lane/class byte while tracing the post-advance `DAT_800758d4` variant 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_script` stores the authored selector in `obj+0x9e`, but `psx_object_lookup_variant_entry` indexes `DAT_800758d4` with `obj+0x94`, the current script word. Two runtime paths (`FUN_80028c94` and `FUN_8002906c`) can reseat the active script from `FUN_8003bc1c(obj) >> 2 & 0xf`, so at least some live `0x0042` selectors `3` and `4` come from heading-based runtime reselection rather than from the original record alone.
- The newly traced consumer side narrows that further. `psx_object_advance_state_script` sign-extends the `DAT_800758d4` lookup result into `obj+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 serialize `DAT_800758d4` as 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. `buildTypeCompanionExtentsMaps` decodes the exported `DAT_800758d4` layer as a `u32 count + packed 3-byte signed extents` table, scene `stateLayers` now preserve those decoded extents per type/state, and each exported PSX scene item and `mapSource` row now carries the resolved `companionExtents` tuple 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+0x94` interacts with the drawable resource at `obj+0x10` after post-spawn reselection. The next useful trace should therefore stay close to those family-specific presentation callers instead of spending more time on generic `DAT_800758d4` consumers.
- The latest dispatch-table pass narrows that again for `0x0042` and `0x0049`: they do not have unique per-type descriptor entries. Both sit inside the same generic descriptor cluster covering `0x003e..0x0050`, and their shared descriptor currently resolves to `psx_spawn_compound_record_advance_state_once`, `psx_object_refresh_main_visible_and_cleanup`, and the newly named `psx_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 `FUN_8002906c` first 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 to `FUN_80028c94` or recomputes heading from motion state before calling `psx_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, and `psx_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 into `obj+0x60/+0x64/+0x68`, `psx_object_quantize_motion_heading16` (`0x8003bc1c`) wraps the current object motion vector, and `psx_quantize_vector_heading16` (`0x8003b980`) reduces that vector to one of 16 heading buckets before the caller maps it back to a `DAT_800758cc` script selector. Combined with the already-verified `psx_object_advance_state_script -> psx_object_lookup_variant_entry` path, this means the art-facing `DAT_800758d4` lookup is driven by the current script word at `obj+0x94`, not directly by the original placement selector stored at `obj+0x9e`.
- The local render-routing wrappers are named now too, which sharpens the practical exporter story. `psx_spawn_compound_record_advance_state_once` (`0x80013618`) and `psx_spawn_simple_record_set_active_flag` (`0x8001372c`) are constructor-side wrappers that immediately push new objects into specific live states, while `psx_object_refresh_main_visible_and_cleanup` (`0x80013688`) is a compact stage-1 projector/cleanup wrapper and `psx_object_advance_state_and_queue_special_visible` (`0x80013758`) is a compact stage-2 wrapper that advances script state and immediately queues through `psx_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+0x10` before the object enters the live update loop; the current script word at `obj+0x94` is then passed straight through `psx_resource_frame_origin_x/y`, `psx_resource_frame_width/height`, and finally into `psx_sprite_resource_submit_frame` or `psx_image_table_submit_frame` during 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_descriptor` creates the drawable resource from the per-type art descriptor, using `image_resource_bind_vram_slot` for single-image type-4 resources and `image_bundle_load_to_vram` for 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`, and `psx_main_visible_list_get_sorted_slice` are the concrete helpers behind the main visible-object pass that `psx_draw_world_visible_passes` submits before the stage-2 queue.
- Practical consequence for the viewer plan: the exported placement selector is now firmly only an input, not a final art choice. For unresolved families the next bridge must model at least part of the post-spawn interaction/reselection lane before a `type -> frame` rule can be trusted, otherwise the viewer will keep drawing structurally correct but still unreadable placeholder-heavy scenes.
- `JL-9` already 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-9` is 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:
1. Revisit the executable loader chain around the `LSET*.WDL` stream consumer and name the section families loaded into `DAT_800678f4`, `DAT_80067720`, `DAT_800758cc/d0/d4/d8`, `DAT_800675f8`, and `DAT_8006769c`.
2. Prove which loaded section is the small top-level object/dispatch list and which section holds the actual bulk map substrate.
3. Recover the format and semantics of the compressed blob that `FUN_8003b00c` inflates into the `0x3e00` level buffer.
4. Tie one concrete subordinate record family to the constructor inputs that feed object `+0x3c/+0x40/+0x44` as `16.16` fixed-point coordinates.
5. Recover the bundle/frame binding rule for map placements well enough to stop relying on broad candidate pairing.
6. Recover the draw-order or layer rule used when multiple map records overlap.
7. Validate the corrected multi-section schema on at least `L0.WDL` and `L1.WDL` so the decode is not overfit to one level.
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:
1. Continue from the already identified texture draw helpers and the caller path that reads palette override metadata from the object field currently described as `+0xa0` in the notes.
2. Determine whether the placement record itself, a second-stage runtime header, or a side table supplies the override palette index.
3. Reconcile the live VRAM `row 0xF0 / x=0` success case against the on-disk palette blob so the export path can reproduce the runtime source instead of only matching dumps.
4. Identify whether different bundle modes or resource classes use different CLUT selection rules.
5. Add exporter-side palette metadata that preserves both bundle default palette and resolved placement palette.
6. 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:
1. Define one PSX scene format version that keeps raw decode fields visible while still fitting the current viewer's atlas-plus-scene model.
2. Export one minimal but real PSX map scene from the solved map schema and load it through the existing viewer path.
3. Compare the rendered result against in-game screenshots, captured VRAM/framebuffer evidence, or clearly identifiable room geometry.
4. Tighten the exporter until one map reads coherently before trying to bulk-export the entire disc.
5. Only after a coherent single-map success, generalize to more `LSET` maps 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:
1. Locate the PSX weapon-name table and the code/data structure that indexes into it.
2. Identify the item or weapon definition row for `JL-9`, including ammo type, flags, and any inventory/equipability markers.
3. Trace all code and data references to that row: mission rewards, cheats, debug grants, pickups, shop/loadout flow, or scripted usecode equivalents if present.
4. Check whether `JL-9` appears in the pre-alpha build under the same index and whether its surrounding data differs from retail.
5. Identify the sprite by following the weapon/item definition to the bundle/frame or icon resource it uses.
6. 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 whether `JL-9` is real
- the acquisition or unlock path if one exists
- the best supported sprite or bundle match
## Recommended Execution Order
1. Finish map-record closure enough to bind placements to the right art.
2. Replace the current `.cache` runtime-record probe premise with the corrected multi-section WDL model, then recover the runtime type/resource lookup that can replace the still-provisional `u0 -> bundle index` rule with real art binding.
3. Get one map loading coherently in the existing viewer.
4. After the viewer path is grounded, use the now-stronger bundle identification flow to close `JL-9` sprite identity and availability.
## Immediate Next Batch
1. In Ghidra, tighten the section-family naming around `DAT_800678f4`, `DAT_80067720`, and the candidate `DAT_8006b5d8` source so the current `section0_*` labels can be promoted from exporter-safe names to exact loader names.
2. Record which helpers read `DAT_80067720`, which apply the separate `DAT_80067794` runtime-header block, and which helpers read the decompressed `DAT_8006769c` buffer now that the offline decode path is present in the cache.
3. Compare the rebuilt all-map exports against recognizable rooms and decide whether the remaining missing structure now lives mainly in the decoded `DAT_8006769c` buffer or in still-unrendered subordinate tables.
4. 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_800758d8` comes from its own late descriptor stream, while `DAT_800758d0/cc/d4` belong to the adjacent `psx_load_type_state_banks` blobs. The remaining open question is the exact sub-split inside those state-bank blobs, not whether they come from the decompressed level-state lane.
5. Recover an actual bundle/frame reference from the per-type template payloads or their consumers so the exporter can replace the now-disproven scan-order bundle fallback with a verified type-to-art rule.
Current delta: the template bank selection is now stronger and already recovers real art for a first subset, but the still-missing families need the stage-1/stage-2 object draw path plus `DAT_800758cc/d4` state interpretation, not more HUD/overlay decoding.
Current delta: stage 2 is no longer hypothetical. The next renderer-improvement candidate is to expose/export the queued-object lane that feeds `FUN_80041144`, because the executable now clearly maintains it separately from the main visible list.
Current delta: the unresolved families are also clearly dynamic now because `psx_object_lookup_variant_entry` reruns after `psx_object_advance_state_script`; the next verified art-binding pass should therefore sample post-jump state, not only constructor-time selectors.
6. Split section-0 placements into at least three executable-backed render classes: world-facing geometry/object placements, animated runtime-only objects, and clearly non-map-facing UI/talk assets such as the portrait bundles currently surfacing through fallback art matching.
7. Decode the `psx_object_advance_state_script` sentinel 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: `fffe` is closed as an audio/effect dispatch through `FUN_8004061c`, `fffd` is the direct indexed jump, and `fffc/fffb` are now separated as the immediate subsidiary-switch versus scan-forward subsidiary-switch pair.
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.md` or another dedicated file under `docs/psx/`.
- Keep this file as the active plan and update it when a major blocker closes or the execution order changes.
- When `JL-9` closes cleanly, give it its own short note under `docs/psx/` instead of leaving it as one bullet in a larger map note.