Crusader_Decomp/docs/raw-000a-000d.md
MaddoScientisto daa363c3d2 Add 'annotate-usecode' command to import USECODE IR JSON annotations
- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors.
- Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option.
- Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra.
- Enhanced JSON schema to include response structure for the new command.
2026-03-24 18:14:20 +01:00

443 lines
72 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Raw 000a & 000d: Tracked Handles, Cache Manager & Proximity Buckets
Content extracted from `crusader_decompilation_notes.md`. Covers the 000d proximity/visibility bucket cluster, 000a tracked-handle table, generic cache manager, seg082 allocator, seg137/138 palette helpers, and seg004/seg005 startup paths.
---
## Raw 000d Proximity/Visibility Bucket Cluster
Small conservative rename batch from the `000d:cc00-d413` region.
| Address | Name | Evidence |
|---------|------|---------|
| `000d:cc00` | `entity_compute_proximity_or_visibility_bucket` | Returns bucket `0x40` for null or on-screen entities (`entity_projected_bbox_overlaps_viewport`), else computes a distance bucket from the current reference entity at `0x7e22` with thresholds `0x17d`, `0x281`, `0x3c1` mapping to `0x32`, `0x20`, `0x10`, `0x08` |
| `000d:d413` | `entity_refresh_recent_proximity_or_visibility_buckets` | Walks the last four active records in the `0x69ac` array, recomputes the same bucket, stores it back to each entry, and calls `000a:6343` when the bucket changes |
| `000d:cdd0` | `tracked_entity_bucket_prune_invalid_entries` | Walks the `0x69ac` array, validates backing handles through `000a:637a`, and clears entry handles to `0xffff` when the backing object is gone |
| `000d:cd62` | `tracked_entity_bucket_find_free_main_slot` | Finds the first free entry in the main portion of the `0x69ac` array (`0 .. count-4`) |
| `000d:cd9a` | `tracked_entity_bucket_find_free_aux_slot` | Finds the first free entry in the auxiliary tail portion of the `0x69ac` array (`count-4 .. count-1`) |
### Supporting caller notes
- `000d:ce1e` populates one `0x69ac` entry by reserving a free slot, computing the initial bucket through `entity_compute_proximity_or_visibility_bucket`, storing both current and previous bucket fields, then allocating/linking the backing handle through `000a:5f36`.
- `000d:d409` is a thin wrapper that only calls `entity_refresh_recent_proximity_or_visibility_buckets`.
- `000d:cfad` is an update-or-allocate helper for `(param_1,param_2)` pairs: it tries to update an existing tracked entry through `000a:606a`, clears dead entries, and falls back to `000d:ce1e` allocation when no live match remains.
- `000d:cec5` is the auxiliary-slot allocator: it prunes invalid entries, uses `tracked_entity_bucket_find_free_aux_slot`, tags the new entry with byte `+0x0a = 1`, and seeds its handle via `000a:5f36(..., flag=1)`.
- `000a:606a` = `tracked_entity_bucket_handle_update_or_alloc` — updates the backing handle for an existing tracked bucket entry when possible, or falls back to allocation via `000a:5f36` if the handle has gone stale.
- `000d:d350` = `tracked_entity_bucket_set_value` — finds a tracked `(entity_id, entity_ref)` entry and pushes a new bucket value into its backing handle through `000a:6343`.
- `000d:d10b` = `tracked_entity_bucket_clear_ref_field` — clears only the `+0x02` reference field for all matching entries.
- `000d:d151` = `tracked_entity_bucket_remove_by_ref` — marks matching entries' backing handles for removal and clears the local entry handle/reference fields.
- `000d:d1b1` = `tracked_entity_bucket_remove_tagged_by_ref` — same removal path, but only for entries whose byte `+0x0a` tag is set.
---
## Raw 000a Tracked-Handle Table
The `0x4673` table is the backing handle registry for the `0x69ac` tracked-entry bucket subsystem. That client layer sits on top of a separate generic cache manager rooted at `0x4688..0x46b7`.
| Address | Name | Evidence |
|---------|------|---------|
| `000a:5f02` | `tracked_entity_handle_find_slot` | Linear scan over 12 entries in the `0x4673` table for a matching 32-bit handle id |
| `000a:602b` | `tracked_entity_handle_is_live` | Returns true only when a handle exists in `0x4673` and its flag word at `+0x0a` does not have bit `0x0002` set |
| `000a:60eb` | `tracked_entity_handle_mark_remove` | Sets bit `0x0002` in the handle-table flag word and dispatches through the unresolved cleanup path |
| `000a:612e` | `tracked_entity_handle_mark_remove_all` | Iterates all 12 handle-table entries and marks each live handle for removal |
| `000a:6167` | `tracked_entity_handle_alloc_slot` | Allocates a slot in one of two ranges (`0..7` or `8..11`) depending on the aux flag; when full, wraps in a ring and evicts via `tracked_entity_handle_mark_remove` before reusing the slot |
| `000a:6228` | `tracked_entity_handle_prune_removed` | Reaps entries previously marked with bit `0x0002`, clears dead slots, and refreshes high-index entries through `000a:6b2d` |
| `000a:63bc` | `tracked_entity_handle_find_by_entity` | Finds the first live handle-table entry whose key/entity word at `+0x04` matches the requested entity id |
### Handle entry layout (stride `0x0c`)
| Offset | Field |
|--------|-------|
| `+0x00` | 32-bit handle id |
| `+0x04` | key/entity id |
| `+0x06` | class/group/source-style selector |
| `+0x08` | current bucket/value |
| `+0x0a` | flags (`bit0` = aux-slot allocation, `bit1` = pending removal) |
### Thin public wrappers
| Address | Name |
|---------|------|
| `000a:5276` | `entity_bucket_track_default_main` — gated by `0x45aa`; creates or refreshes a main-slot tracked handle with bucket `0x40` and selector `0xff` |
| `000a:5294` | `entity_bucket_track_main` — same path, but takes the bucket value as an argument for the main-slot range |
| `000a:52d0` | `entity_bucket_track_default_aux` — aux-slot variant with default bucket `0x40` |
| `000a:52ee` | `entity_bucket_track_aux` — aux-slot variant with explicit bucket argument |
---
## Raw 000a Generic Cache Manager
Follow-up analysis of `000a:6b2d` and the `0x4688..0x46b7` globals shows that this region is a generic cache manager used by the tracked-handle layer, not part of the tracked-entity subsystem itself.
| Address | Name | Evidence |
|---------|------|---------|
| `000a:6b2d` | `cache_lookup_or_load_entry_by_id` | Fast-paths the last id via `0x46af/0x46b1`, otherwise searches `0x469d`, evicts older cache slots until there is room under byte budget `0x46a5`, allocates a block from the free-list, clears/initializes the payload, records the id, and dispatches through the loader interface at `0x468c` |
| `000a:6a95` | `cache_release_entry_by_slot` | Releases a cached slot by index, clears any client references through `000a:62d8`, frees its backing block through `cache_free_block_by_slot`, and marks the slot id in `0x469d` as unused (`0xffff`) |
| `000a:6d07` | `cache_alloc_block_for_slot` | Allocates or splits a block from the free-list anchored at `0x4688`, tags it with the owning cache slot index, and updates the in-use byte count at `0x46a9` |
| `000a:6f4d` | `cache_free_block_by_slot` | Finds the free-list node for a cache slot, marks it free, subtracts its size from `0x46a9`, and coalesces adjacent free blocks |
| `000a:67d9` | `cache_shutdown` | Tears down the generic cache manager: flushes/reset state, frees slot arrays at `0x4699/0x469d/0x46b3`, frees the free-list container at `0x4688`, and closes backing state at `0x4691` |
| `000a:6898` | `cache_set_loader_interface` | Installs the backend loader/callback interface pointer at `0x468c` |
### Cache globals
| Address | Name | Notes |
|---------|------|-------|
| `0x4688` | free-list/block-list head | Used by `cache_alloc_block_for_slot` and `cache_free_block_by_slot` |
| `0x468c` | cache_loader_interface | Backend callback table; `+0x34` = size query, `+0x0c` = load/bind callback |
| `0x4695` | arena base pointer | Base for the raw cache payload arena |
| `0x4699` | per-slot payload-pointer table | |
| `0x469d` | per-slot cached id table | `0xffff` = unused |
| `0x46a5` | byte budget / arena capacity | |
| `0x46a9` | bytes currently in use | |
| `0x46af/0x46b1` | fast-path cache | Last requested id and slot index |
| `0x46b3` | per-slot block metadata mirror | Used when releasing or refreshing slots |
---
## Follow-up: Cache Init and Runtime State
| Address | Name | Notes |
|---------|------|-------|
| `000a:6600` | `cache_init` | Stores slot count in `0x46ad`; allocates per-slot payload-pointer table; seeds each slot; queries/derives arena size; allocates arena backing object at `0x4691`; allocates per-slot metadata mirrors; initializes free-list head at `0x4688`; calls `cache_reset_runtime_state` |
| `000a:68aa` | `cache_reset_runtime_state` | Shared cache reset/bootstrap helper called from `cache_init`, `cache_shutdown`, and external reset paths. Allocates per-slot arena-header nodes, rebinds slot pointers to arena base, clears the cached-id table, seeds the free-list head, and resets `0x46a9` (bytes in use) plus `0x46af` (last-id fast path) |
| `000a:703e` | `cache_compact_arena_blocks` | Compacts live cache arena blocks into earlier free holes when allocation would fail, updates per-slot payload pointers, and merges adjacent free-list headers afterward |
---
## Follow-up: Tracked-Handle Table Init/Shutdown
| Address | Name | Notes |
|---------|------|-------|
| `000a:5e00` | `tracked_entity_handle_table_init` | If `0x4672` is clear, allocates `0x90` bytes at `0x4673/0x4675`, aborts through `runtime_init_or_abort` on failure, calls `000a:577d` and local helper `000a:5e95`, then sets `0x4672 = 1` |
| `000a:5e59` | `tracked_entity_handle_table_shutdown` | Matching teardown for `tracked_entity_handle_table_init` |
| `000a:5e95` | `tracked_entity_handle_table_clear_and_dispatch` | When `tracked_entity_handle_table_active` is set, zeroes the full `0x90`-byte handle table at `0x4673`, resets adjacent local state at `0x4677/0x4679/0x467b`, then dispatches through the remaining thunked follow-up path |
| `000a:5339` | `tracked_entity_handle_mark_remove_all_if_enabled` | Thin gate wrapper that only forwards to `tracked_entity_handle_mark_remove_all` when `tracked_entity_bucket_system_enabled` is set |
Table globals: `0x4672` = `tracked_entity_handle_table_active`, `0x4673/0x4675` = `tracked_entity_handle_table` (12 entries × `0x0c` = `0x90` bytes).
---
## Follow-up: Tracked Bucket System Init/Shutdown
| Address | Name | Notes |
|---------|------|-------|
| `000a:5186` | `tracked_entity_bucket_system_init` | Allocates a rotating buffer via `0009:3600`, lazily creates `tracked_entity_bucket_backend_object` through `0009:5600` when absent, installs that object into `cache_loader_interface`, allocates the tracked handle table via `000a:5e00`, allocates the 32-entry `0x69ac` bucket array via `000d:cca3(0x20)`, then sets `tracked_entity_bucket_system_enabled` |
| `000a:538e` | `tracked_entity_bucket_system_init_if_configured` | Only calls the init routine when config/feature gate `0x89f4` is non-zero |
| `000a:5223` | `tracked_entity_bucket_system_shutdown` | Tears down the tracked handle table, frees the `0x69ac` bucket array, calls backend-object vtable slot `+0x38` with `(3, backend_object)`, clears `tracked_entity_bucket_backend_object`; called from the wider engine teardown routine at `0004:621b` |
System globals: `0x45aa` = `tracked_entity_bucket_system_enabled`, `0x45ab/0x45ad` = `tracked_entity_bucket_backend_object`.
Public thin gate wrappers that feed the `0x69ac` tracked-entry layer:
- `0005:3b34` = `tracked_entity_bucket_alloc_main_if_enabled`
- `0005:3b53` = `tracked_entity_bucket_alloc_aux_if_enabled`
- `0005:3b72` = `tracked_entity_bucket_remove_by_entity_and_ref_if_enabled` → forwards into `000d:d086 = tracked_entity_bucket_remove_by_entity_and_ref` when `0x45aa` is set.
---
## Follow-up: Backend Object Constructor
| Address | Name | Notes |
|---------|------|-------|
| `0009:5600` | `cache_backend_object_init` | Allocates a `0x20`-byte object when caller passes null; initializes embedded DOS file-handle state via `dos_file_handle_init`; seeds internal method-table / state fields at object offsets `+0x08`, `+0x0c`, `+0x10`, `+0x14`, `+0x16`, `+0x18`, and `+0x1c`; dispatches through the object method table during construction; returns the object pointer cached at `0x45ab/0x45ad` |
Verified callback roles inside `cache_lookup_or_load_entry_by_id`:
- backend vtable `+0x34` = size query callback for a cache entry id (used before allocation/eviction)
- backend vtable `+0x0c` = load/bind callback that populates the newly allocated slot buffer for the requested id
---
## Follow-up: External Reset Paths
- The path around `0004:25a9` classifies as an external reset sequence: it calls `cache_reset_runtime_state`, then `tracked_entity_handle_table_clear_and_dispatch`, then continues through additional tracked-entry/cache-side refresh helpers (`000d:cd22`, `000d:44b3`, `0006:ae66`, `0006:ae00`, etc.).
- The path around `0004:eb80` is a conditional tracked-bucket reset/update sequence: when `tracked_entity_bucket_system_enabled` is set, it calls `tracked_entity_handle_mark_remove_all_if_enabled`, then `tracked_entity_handle_table_clear_and_dispatch`, then `cache_compact_arena_blocks`, before resuming its outer flow.
---
## Follow-up: Repaired seg004 Reset-Path Function Objects
| Address | Name | Notes |
|---------|------|-------|
| `0004:2592` | `runtime_cache_reset_sequence` | Calls `0008:7bfe`; calls `game_mode_init(*(0x27c4))`; calls import-resolved site `0004:25a4`, now verified from the separately imported `ASYLUM.DLL` as ordinal `24` = `_ASS_StopAllSFX`; then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers. Known caller: `0004:262d` inside the tiny wrapper at `0004:2620`, which sets byte `+0x40` on the object at `0x6828` before invoking the reset sequence. |
| `0004:eb1f` | `entity_dispatch_entry_ctor_0f3a_with_cache_reset` | Allocates/initializes an entity dispatch entry; stamps entry type `0x0f3a`; stores its two word payload fields; runs local setup through embedded helper at `0004:ebf4` (which dispatches `entity_dispatch_reset_all(*0x7e22, 0x00f0)` and — when the local flag plus global `0x0ee1` allow it — allocates a type `0x0f5e` dispatch entry and passes it to `entity_pair_sync_b`); when `tracked_entity_bucket_system_enabled` is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through `0009:b1c3` in phase `0`. |
| `0004:ea00` | `entity_dispatch_entry_alloc_type_0f5e` | Reuses the incoming FAR pointer when non-null; otherwise allocates `0x33` bytes through `mem_alloc_far`; initializes the entry through `entity_dispatch_entry_init`; stamps the entry type word at `+0x00` to `0x0f5e`. |
---
## Follow-up: seg082 Allocator Cluster
| Address | Name | Notes |
|---------|------|-------|
| `0009:a229` | *(size-only wrapper)* | Public size-only wrapper around the seg082 allocator. Lazily initializes the allocator on first use through `0009:bcb9`, then calls `allocator_try_alloc_from_head_table(size, default_tag, 0xff)`. |
| `0009:bcb9` | *(lazy initializer)* | One-time lazy initializer. Parses an optional `-x` tuning value from the PSP command line, clamps derived percentage into `0x14..0x50`, seeds local seg082 helpers, then sets init flag `0x4096 = 1`. |
| `0009:b06b` | `allocator_try_alloc_from_head_table` | Validates requested size, reserves a temporary work token through `0009:e15f`, scans the `0x8724` allocator head table in `0x0c`-byte entries via `allocator_head_try_alloc_block`. On success, commits the result through `0009:e2b4`, clears failure flag `0x4098`. When a pass does not find a fit, interleaves up to two finalize phases through `allocator_phase_finalize_pass(phase)` before the final retry. |
| `0009:a336` | `allocator_head_try_alloc_block` | Per-head first-fit allocator. Normalizes requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds `0x0a` node header overhead, enforces minimum of `0x10` bytes. Walks the node chain for one allocator head until it finds a free span large enough. On success, unlinks the chosen free node, either consumes it whole or splits off a remainder when `>= 0x10` bytes remain. On failure, returns `0`. |
| `0009:a5d1` | `allocator_head_free_block` | Per-head free paired with `allocator_head_try_alloc_block`. Rebuilds the node header from a payload pointer (`payload - 0x0a`), validates the owner/tag word, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible. |
| `0009:b224` | `allocator_free_block_by_ptr` | Converts the payload pointer back through local header helpers, scans the `0x8724` head table for the owning range, dispatches to `allocator_head_free_block`, and aborts if no owning head is found. Known wrappers `0009:a24f` and `0009:a27a` are small checked entry points into this path. |
| `0009:b1c3` | `allocator_phase_finalize_pass` | Accepts phase bytes `0` or `1`. Forwards that byte twice to the object rooted at `0x4588` through vtable slot `+0x08`. Then sweeps the allocator head table at `0x8724` up to the active head count at `0x879c`, calling `allocator_head_finalize_sweep` on each entry. |
| `0009:af87` | *(free-space probe)* | Walks the node chain rooted at `0x8724`. For each node, accumulates `node_size - 9` into a running total and tracks the largest single free block. Used by `cache_init` and seg013 path at `0004:833b`. |
| `0009:a961` | *(per-head finalize sweep)* | Walks one `0x8724` head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain. |
**Allocator head table structure:**
- `0x8724` = array of `0x0c`-byte allocator heads
- `0x879c` = active head count / table limit
- Per-node size/value encoding manipulated through `0009:c628` and `0009:c6ae`, which read/write a packed 32-bit quantity split across `word + byte + byte` fields
---
## Follow-up: seg137 Palette and Dispatch-Entry Helper Family
A coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.
| Address | Name | Evidence |
|---------|------|---------|
| `000d:85da` | `vga_palette_set_all_black` | Allocates a `0x100`-entry palette buffer filled with zero RGB triplets, writes it to VGA, frees the scratch buffer. (Previously mis-named `map_object_set_dirty_flag`.) |
| `000d:8653` | `vga_palette_set_all_white` | Same shape as black — all three RGB components initialized to `0x3f`, then written through `vga_palette_write`. |
| `000d:86cc` | `vga_palette_set_all_rgb` | Takes caller-supplied RGB bytes, replicates them across a `0x100`-entry palette buffer, writes the result to VGA, frees the scratch palette. |
| `000d:82ea` | `dispatch_entry_create_black_palette_state_active` | Builds a runtime-state dispatch entry of type `0x051e` from a black `0x100`-entry palette buffer; first sets `g_active_dispatch_entry_farptr[+0x40] = 1`. |
| `000d:8a47` | `dispatch_entry_create_black_palette_state` | Same as above without marking the active dispatch entry. |
| `000d:83be` | `dispatch_entry_create_grayscale_palette_state_active` | Reads the current VGA palette, normalizes each triplet by copying the first channel across all three RGB bytes, then builds a runtime-state dispatch entry from that grayscale palette while marking the active dispatch entry. |
| `000d:875d` | `dispatch_entry_create_solid_palette_state_active` | Validates `0..0x3f` RGB inputs, fills a scratch `0x100`-entry palette buffer with that solid color, builds the same `0x051e` runtime-state dispatch entry, marks the active entry. |
| `000d:88b2` | `dispatch_entry_create_solid_palette_state` | Same as above without marking the active dispatch entry. |
Additional caller-side comments (not renamed) added on:
- `000d:84f4` — current-palette dispatch entry paired with a second object of type `0x68bf` through `entity_pair_sync_b`
- `000d:89c6` — parameterized current-palette runtime-state wrapper with active-state flags
---
## Follow-up: seg138 Caller-Side Dispatch-Entry Emission Helper
`FUN_000d_938c` (`000d:938c-000d:9583`) — a real caller-side helper with an evidence-preserving decompiler comment added in Ghidra instead of forcing a speculative rename.
Current verified behavior:
- When the mode/global gate is not already in the `0x13:0x0008` state and entity byte `+0x33` is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type `0x051e`, and initializes runtime state through `entity_dispatch_entry_init_runtime_state` with entry kind `0x3c`.
- Later in the same helper it constructs a second dispatch entry from the current palette globals at `0x4e4:0x4e6`, again sets type `0x051e`, and initializes runtime state with entry kind `0x14` and active-state parameters `(1,0,1)`.
- Both created entries are polled until their runtime flag word clears bit `0x0002`, after which the helper redraws the global sprite path, syncs display-state byte `0x58e` from the entity when the global display object exists, calls `FUN_0006_16e1`, clears `g_active_dispatch_entry_farptr[+0x40]`, and finally dispatches through the input object's vtable slot `+0x08`.
---
## Follow-up: seg005 Startup/Display Orchestration
| Address | Name | Notes |
|---------|------|-------|
| `0004:60c0` | `startup_display_transition_prepare` | Startup/display transition prepare step: broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot `+0x0c`, drives the sprite/object lane through `0x4f38`, creates the default active dispatch entry through `active_dispatch_entry_create_default`, programs mouse interrupt state via seg056 `INT 33h` wrappers, then hands off into `startup_display_transition_driver`. |
| `0004:1e00` | `startup_display_transition_driver` | Non-return startup/display transition driver: raises the shared active-dispatch hold byte around the seg049 watch/controller lane, then clears it before the seg080 redraw and seg126 follow-up path. |
| `000d:7600` | `active_dispatch_entry_mark_enabled` | Marks the active dispatch entry enabled |
| `000d:760e` | `active_dispatch_entry_mark_disabled` | Marks the active dispatch entry disabled |
| `000d:761c` | `active_dispatch_entry_create_default` | Creates the default active dispatch entry |
Current verified caller-side detail:
- `startup_display_transition_prepare` now has enough exact instruction evidence to pin the seg108 lane down more tightly. Window `0004:618e..620c` calls the `0x4f38` sprite/object helpers in a stable sequence around the shared active-entry creation: seg108 `000b:1e39`, `000b:2492`, and `000b:26bd` run before `active_dispatch_entry_create_default`, and seg108 `000b:2706` runs again immediately before the handoff into `startup_display_transition_driver`.
- The same seg108 window now shows a local bounded counter/stack contract instead of reuse of the validated caller object. `000b:26bd` increments object word `+0x196` up to `7` before calling the common local helper at `000b:2592`, and `000b:2706` reads one prior slot from `+0x186`, decrements `+0x196`, and replays the same helper before `startup_display_transition_driver` takes over.
- The seg108 helper pair is now named too: `sprite_object_push_state_word` (`000b:26bd`) increments the bounded local stack depth at `+0x196`, stores the incoming word into the per-object stack at `+0x186`, refreshes local sprite state through `000b:2592`, and replays redraw when the object was already marked dirty; `sprite_object_pop_state_word` (`000b:2706`) returns the previous top word, decrements the same bounded depth, and reapplies the new top through that same helper. This makes the `0x4f38` lane read more like a self-contained sprite/object state stack than a reused validated caller object.
- `startup_display_transition_driver` now has exact hold-token ordering too. Window `0004:2013..212c` raises `g_active_dispatch_entry_farptr[+0x40]`, dispatches the seg049 watch/controller object at `0x2bd8` through vtable slot `+0x2c`, runs the intervening transition call at `0004:eece`, and then clears the same shared hold byte again just before the seg080 redraw pair and seg126 follow-up.
- Upstream caller tracing now shows that the four `0004:eece` call shapes are chosen from one startup switch-parser lane rather than from a named local phase enum. Window `0004:63c1..66fb` walks an argv-like table against a local dispatch/jump table, and the `0004:64ff..65c1` case loads globals `0x84a/0x84c/0x84e/0x850` plus scalar `0x856`; `startup_display_transition_driver` later fans those values into the `0004:2049`, `0004:20b3`, `0004:20c6`, and `0004:20fe` call variants. The other direct caller anchors at `0004:2657`, `000c:8786`, and `000c:742c` all remain inside the same startup/display presentation-handoff family, so the safest output is still tighter caller-family semantics rather than a new neutral state label.
- The seg049 and seg108 globals are now better separated by direct decompile evidence rather than only call-window correlation. `watch_entity_controller_dispatch_if_present` confirms that `0x2bd8` is a real controller object with active vtable dispatch at slots `+0x2c` and `+0x30`, while `sprite_object_set_flag40_if_present` and `sprite_object_clear_flag40_if_present` show that `0x4f38` is a separate global object lane whose immediate local contract is only bit `0x40` in object word `+0x32`.
- The seg127 fade-controller ownership is also one step tighter in the same lane. `transition_preentry_setup_resources` resets `0x630a` at `000c:c855`, `transition_preentry_step_script` now has a verified early gate at `000c:ca25` that yields to the fade controller whenever `0x630a` is active, and `transition_palette_fade_begin` at `000c:cdca` explicitly installs palette source/range/step state into `0x630e..0x6316`, asserts `0x630a`, and kicks one immediate fade tick.
- Fade direction is now pinned to seg126 script-control bytes rather than the outer seg005 wrappers. Inside `transition_preentry_step_script`, control byte `0x5e` reaches `palette_fade_begin_full_down` at `000c:cb06`, while control byte `0x26` reaches `palette_fade_begin_full_up` at `000c:cd1a`; control byte `0x2a` shares the same post-fade bookkeeping path after the full-up call.
- The upstream producer path for the remaining seg126 control bytes is now tighter too. `transition_preentry_setup_resources` composes one path from the mutable base at `0x6aa:0x6ac` plus local name buffers (`0x631c`, `0x6335`) through the seg072 slash-aware path helper `0009:3600`, opens that file through `file_handle_alloc_init_and_open`, allocates a buffer of the returned size, reads the full payload into `0x6301:0x6303`, and seeds `0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318` before the loop starts. Current best reading is therefore `file-backed transition script/control buffer`, not locally synthesized opcodes.
- The adjacent seg126 selector lane is now classified tightly enough for conservative renames. `transition_file_family_select_and_refresh` (`000c:afa5`) keys object field `+0x49` through values `0`, `1`, and `4`, composes three sibling filenames from the inherited base `0x6aa:0x6ac` plus shared stem `0x621c` with suffix buffers `0x6223`, `0x622d`, and `0x6237`, loads the chosen file into object `+0x520`, and then runs the same redraw/palette/input refresh path. The same helper uses `field49==2` as a direct `vtable[0x3c]` callback branch and `field49==-1` as a normalize-back-to-zero state.
- The local wrappers around that selector now sharpen the caller model without forcing a stronger UI label. `transition_file_family_advance_on_anim_tick` (`000c:b153`) increments `+0x49` when the polled byte at `[param_2+0x14+0xa]` is clear and then re-enters the selector, while `transition_file_family_input_key_handler` (`000c:b199`) maps Left/Right and `n/N` into previous/next selector steps, uses `e/E` plus repeated `-` to force selector state `4`, and otherwise exits through `vtable[0x3c]`.
- This closes the narrow `+0x49` question as a local three-way file-family selector lane, but it still does not justify a stronger UI label for the paired `0x8c5c/0x8c60` renderer presets or the sibling seg127 fade inputs.
- The remaining `transition_preentry_step_script` opcodes now have stable local mechanics even though the higher-level text semantics are still open. Control byte `0x21` consumes the next script word into `SI` and advances `0x62ff` by two, which makes it the current baseline/start-position loader for later text draws. Control byte `0x40` renders one null-terminated entry from the same script buffer through renderer object `0x8c5c:0x8c5e`, while control byte `0x24` mirrors that behavior through `0x8c60:0x8c62`; both paths measure width through the renderer vtable, draw through seg088 `000a:30d7`, blit through seg080 `0009:943a`, advance `SI` by rendered width plus four, and then scan forward to the next opcode byte. Control byte `0x23` sets local completion byte `0x62fe = 1` and returns, so the outer shell exits on the next loop test instead of iterating further.
- Secondary renderer-factory sampling keeps the `0x8c5c` / `0x8c60` split conservative. Other sampled `000a:9748` xrefs use different adjacent preset pairs such as `0x0d/0x0c` at `0007:df30/df3f` and `0x0c/0x0f` at `0008:47c9/4851`, while no sampled caller reproduced the exact `0x10/0x11` startup pair outside `transition_preentry_setup_resources`. That supports keeping these as paired preset text renderers without forcing a title/body or normal/highlight label.
- The missing seg126 step body at `000c:ca1d` still cannot be split out safely because `create_function_by_address` collides with the existing oversized overlap namespace, so this pass preserved the recovery as a decompiler comment instead of forcing a destructive boundary repair. Current best reading is still that `000c:ca1d..cd34` is the real `transition_preentry_step_script` body and that `000c:cd35` starts the fade-tick helper.
---
## Follow-up: `0x31a2` Break/Hold Depth and Active Dispatch Ownership
This pass tightened the shared startup/display transition lane enough to preserve the gate semantics directly in Ghidra and to promote the active-dispatch helpers out of an isolated foothold.
### Verified `0x31a2` role
- `0008:a283` increments `0x31a2` while installing one live far-pointer slot record into the seg008 per-index table, and the paired path at `0008:a314` decrements `0x31a2` while clearing that same record.
- `0005:453a` is now commented in Ghidra as a plain getter for the shared `0x31a2` depth word.
- `transition_preentry_run_until_complete_or_abort` (`000c:c9f4`) now decompiles cleanly and confirms the main transition loop runs until either local completion byte `0x62fe` becomes non-zero or `0x31a2` becomes positive.
- The shell around `transition_preentry_step_script` is now tighter too: `000c:ca11` is the direct second exit test in the outer seg126 loop, so a positive `0x31a2` falls straight into `transition_preentry_release_resources` even when local completion byte `0x62fe` is still clear.
- `0004:c24d` and `000c:e4d8` are now tightened as pure busy-wait edge loops on `0x31a2`, while `000c:e546` and `000c:e5c6` are the same break-depth check embedded in local presentation/cleanup loops rather than plain one-shot flag tests.
- The blocking/waiting readers around `000c:e4d8`, `000c:e546`, and `000c:e5c6` treat `0x31a2` as an asynchronous break condition rather than a local state bit: they either busy-wait for a positive edge or abort their local presentation loop early when the depth is already positive.
- Additional caller-side reads at `000d:9304`, `000d:b6b1`, and `000d:c0ee` all use `0x31a2 > 0` to short-circuit or advance local dispatch-entry state, which fits a shared break/hold depth better than a one-shot acknowledge flag. `dispatch_entry_kind2_tick_hold_and_maybe_dispatch` (`000d:92eb`) is still the clearest recovered dispatch-entry example: when a kind-`0x0002` entry is pending, a positive `0x31a2` lets the helper dispatch vtable slot `+0x08` even if the local `+0x40` hold byte is still asserted, after which it decrements that local byte. The newly checked `000d:b6b1` reader is narrower: it advances one local state-`5` branch only when entity byte `+0x78` is set and class/state word `+0x16` carries bit `0x4000`, then optionally runs the seg092 follow-up when the `0x45aa` gate and entity byte `+0x74a` are both active.
Current best neutral description: `0x31a2` is a shared asynchronous break/hold depth maintained by the seg008 install/remove path and consumed by transition/presentation code as a positive-count modal break condition.
### `0x6341` to `0x6828` relationship
- The missing seg126 function object at `000c:c63a` is now created and named `transition_preentry_setup_resources`; its body allocates the paired temporary text-renderer objects at `0x8c5c/0x8c60`, draws the preset `0x10` and `0x11` text variants, loads the file-backed buffer into `0x6301:0x6303`, and seeds the pre-entry state bytes before the main loop starts.
- The paired helper `transition_preentry_release_resources` (`000c:c890`) still handles teardown, but its late branch at `000c:c958` also constructs the transition-local animation object at `DS:0x6341` through `animation_ctor_variant_a`, then immediately sets `g_active_dispatch_entry_farptr[+0x40] = 1` at `000c:c963`.
- `active_dispatch_entry_create_default` (`000d:761c`) still owns the canonical `g_active_dispatch_entry_farptr` installation.
- `dispatch_entry_kind2_tick_hold_and_maybe_dispatch` (`000d:92eb`) now makes that paired readback explicit: when a kind-`0x0002` dispatch entry is pending and either entry byte `+0x40` is already non-zero or the shared break depth `0x31a2` is positive, it dispatches vtable slot `+0x08` and then decrements `+0x40`.
- `FUN_000d_938c` and `entity_cleanup_resources_and_dispatch` both clear `g_active_dispatch_entry_farptr[+0x40]` after their palette/presentation handoff work, which ties the active entry to the same transition-owned presentation lane rather than to an isolated constructor helper.
- `entity_cleanup_resources_and_dispatch` is now tighter on the caller side too. Its first `0x4588` callback emit at `000d:9d5e` is confirmed as the `entity +0x12d/+0x12f` payload-pair path, while the later emit at `000d:a3b7` uses the separate `entity +0x74f/+0x751` pair. Both still sit inside the same palette/watch-controller cleanup body rather than a separate callback-only helper.
- Caller-role alignment is now tighter across the remaining startup/display cleanup bodies. The mis-split seg126 window `000c:6176/619c -> 000c:6226` constructs only a temporary local animation payload, frees it, dispatches the seg049 watch/controller object at `0x2bd8` through vtable slot `+0x2c`, and then clears the shared owner byte `+0x40`; `FUN_000d_938c` similarly waits on two temporary palette/state entries, redraws, clears the same shared hold byte at `000d:958d`, and only then dispatches its caller object through vtable slot `+0x08`; `entity_cleanup_resources_and_dispatch` clears `g_active_dispatch_entry_farptr[+0x40]` at `000d:a1cf` only on the branch where entity byte `+0x737` is set and no temporary object remains, before falling into the same watch/controller dispatch at `000d:a1ed`. That is enough to align them as consumers of one shared presentation hold token around the seg049 lane, but still not enough to justify a single higher-level subsystem rename for `FUN_000d_938c` or the mis-split seg126 body.
### Follow-up: shared owner versus borrowed hold-token model
- `active_dispatch_entry_mark_enabled` (`000d:7600`) and `active_dispatch_entry_mark_disabled` (`000d:760e`) are now verified as tiny wrappers that only write the shared owner byte `g_active_dispatch_entry_farptr[+0x40] = 1/0`; they do not install or replace the owner object.
- `entity_dispatch_entry_init_runtime_state` (`000d:7e00`) now tightens that ownership split further. When it builds a runtime-state dispatch entry, it copies the current shared owner byte at `g_active_dispatch_entry_farptr[+0x40]` into the new entry's local byte `+0x40`; if the new entry stays inactive while a shared owner exists, it raises the shared owner's `+0x40` byte to `1` instead of replacing the owner pointer.
- The paired destructor `entity_dispatch_entry_release_runtime_state` (`000d:8078`) clears `g_active_dispatch_entry_farptr[+0x40]` when the runtime-state entry was marked as owner-propagating (`+0x41 != 0`) or when the entry's local hold byte was never asserted. This matches borrowed hold-state propagation, not separate owner creation.
- `startup_display_transition_driver` now provides the clearest caller-side proof in seg005: it raises `g_active_dispatch_entry_farptr[+0x40]` at `0004:2013` before the seg049 watch/camera path and the `0004:eece` transition call, and clears that same byte again at `0004:2128` before the seg080 display update pair, sprite redraw, and seg126 follow-up thunk `000c:82f9`.
- The still-mis-split seg126 window around `000c:6176/619c` also fits the same model. It makes one mode-dependent `animation_ctor_variant_a` call on a temporary local object, frees that temporary object, reloads the palette, dispatches the `0x2bd8` watch/camera controller through vtable slot `+0x2c`, and later clears `g_active_dispatch_entry_farptr[+0x40]` at `000c:6226`. No canonical owner installation is visible in that body.
- The thin seg005 wrappers at `0005:3c36` and `0005:3c5b` are now confirmed as pure `animation_ctor_variant_a` preset shims. Combined with the seg126 windows above, current best reading is: `DS:0x6341` and the other constructor callsites build transition-local or display-local animation payloads, while `0x6828` remains the shared active-dispatch owner installed elsewhere.
Current best model: `g_active_dispatch_entry_farptr` is a shared owner installed by `active_dispatch_entry_create_default`, and the startup/display transition lane mostly borrows and propagates the owner's byte `+0x40` as a hold/busy token while palette/runtime-state helpers run. The remaining open problem is the exact state/object label behind the seg049 watch/camera path, the seg108 sprite/object lane, and the cleanup branches that consume the same token.
### Current batch: presentation handoff family versus single-owner hypothesis
- The exact late sequencing now supports one stricter neutral read: these bodies behave like a shared startup/display presentation handoff family, but not like one single owner-object family. In `startup_display_transition_prepare`, the validated caller object (vtable `+0x0c`), the seg108 `0x4f38` lane, and the seg049 `0x2bd8` watch/controller lane stay separated by direct instruction windows rather than collapsing into one reused object path.
- `transition_preentry_setup_resources` also tightened the paired renderer role one step further. Window `000c:c659..c6ab` allocates renderer presets `0x10` and `0x11` through seg099 `000a:9748`, stores them at `0x8c5c:0x8c5e` and `0x8c60:0x8c62`, and immediately draws the same seed text buffer `DS:0x631a` at `(0x0a,0x0a)` through both objects. That makes the pair look like two preset text-render variants inside the same temporary presentation lane, not two separate owner objects.
- The shared hold-token consumers now line up more exactly than before. `startup_display_transition_driver` raises `g_active_dispatch_entry_farptr[+0x40]` before the seg049 `+0x2c` dispatch and clears it again before the redraw/follow-up path; `transition_preentry_release_resources` tears down the paired renderers and script buffer, and only on its late completion branch builds local `DS:0x6341` then raises the same shared byte; the mis-split `000c:6176/619c -> 000c:6226` body frees its temporary local animation object, reloads the palette, dispatches `0x2bd8`, and only then clears the shared byte; `FUN_000d_938c` waits on two temporary palette/state entries, redraws, clears the shared byte, and only then dispatches caller vtable `+0x08`; `entity_cleanup_resources_and_dispatch` clears the shared byte only on the late `+0x737` cleanup branch immediately before the same `0x2bd8` dispatch.
- The seg049 controller lane is also slightly tighter locally. `watch_entity_controller_create_global` (`0007:ba00`) delegates to `watch_entity_controller_create` (`0007:ba45`), which stamps type `0x2c2b`, stores the global object at `0x2bd8`, and seeds static row `DS:0x2be4` into `0x39ca[obj+2]`; the common `watch_entity_controller_dispatch_if_present` wrapper (`0007:ba13`) then runs both vtable slots `+0x2c` and `+0x30`. That still supports a real controller object, but not a strong enough state label to rename the wider family.
- The seg108 lane is one step tighter in the same pass. `sprite_redraw_if_needed` (`000b:2492`) remains the redraw-facing helper, while the newly named `sprite_object_push_state_word` / `sprite_object_pop_state_word` pair show that prepare-time use of `0x4f38` is bracketed by a bounded per-object state-word stack at `+0x186/+0x196` rather than by reuse of the validated caller object or the seg049 controller.
Current safest naming conclusion from this batch: keep the existing concrete function names, treat `FUN_000d_938c` and the related seg126/seg138 callers as one shared startup/display presentation handoff family, and defer any stronger single-state or single-owner rename until a caller-side state discriminator appears.
This is enough to treat seg136 as a shared active-dispatch owner/hold-state controller and seg138 as a real cleanup/presentation caller family, even though the final subsystem name is still open.
### Current batch: exact edge waits, interleaved handoff, and broader sprite-stack reuse
- The remaining pure `0x31a2` edge waits are now exact rather than inferred. `0004:c24d` is a two-phase wait that first spins while the shared break/hold depth is non-zero and then spins until it becomes positive again before continuing, while `000c:e4d8` is the simpler positive-edge gate that only waits for `0x31a2 > 0` and immediately returns into the local presentation path.
- `FUN_000d_938c` is now slightly tighter on sequencing even though it still does not justify a rename. The first scratch-palette runtime-state entry (`kind 0x3c`) is only built on the branch where global `0x68e6` is not already in the `0x13:0x0008` mode or entity byte `+0x33` is clear; after that wait completes, the helper clears the seg049 controller bit and dispatches `0x2bd8` through vtable slot `+0x2c`, performs one rectangle/display sync, and only then conditionally builds the current-palette runtime-state entry (`kind 0x14`) before the final redraw, shared hold-byte clear, and caller-object vtable `+0x08` dispatch. That keeps the body inside the same presentation-handoff family while making it less plausible as a single-owner constructor.
- Additional seg108 push/pop callsites now show that the `0x4f38` lane is reused outside the startup prepare shell. Windows `000b:9bb8/9bda` and `000b:9c3c/9c6e` bracket transient seg101 presentation helpers with the same `sprite_object_push_state_word` / `sprite_object_pop_state_word` pair on global sprite object `0x5e82:0x5e84`, while `000c:831f`, `000c:8845`, `000c:8909`, and `000c:a05f` pop that same state stack during later UI or object-cleanup flows. This strengthens the neutral reading that `0x4f38` is a generic sprite-object state stack, not the validated prepare-time caller object and not the `0x2bd8` watch/controller object.
### Current batch: shared hold-token ownership closure
- The highest-value remaining ownership question in the startup/display lane is now narrow enough to close without a speculative rename. `active_dispatch_entry_create_default` remains the canonical installer for `g_active_dispatch_entry_farptr`, while the later seg005/seg126/seg138 bodies only borrow or propagate the shared owner byte `+0x40` as a transition/presentation hold token.
- The set/clear sites now line up as one borrowed-hold family instead of competing owner installs. `startup_display_transition_driver` raises the shared byte at `0004:2013` before the seg049 controller dispatch and clears it at `0004:2128`; `transition_preentry_release_resources` raises it only on the late completion branch at `000c:c963` after building temporary `DS:0x6341`; `FUN_000d_938c` clears it at `000d:958d` after both temporary palette/state waits and redraw; `entity_cleanup_resources_and_dispatch` clears it at `000d:a1cf` only on the late cleanup branch immediately before the same seg049 controller dispatch.
- The seg049 and seg108 lanes stay separate in the exact places that matter for ownership. `watch_entity_controller_dispatch_if_present` (`0007:ba13`) confirms `0x2bd8` is a real controller object dispatched through vtable slots `+0x2c/+0x30`, while `sprite_object_clear_flag40_if_present` / `sprite_object_set_flag40_if_present` (`000b:2b08` / `000b:2b20`) only toggle bit `0x40` in the separate global sprite/object at `0x4f38 + 0x32`.
- The owner/borrow split also remains visible inside the dispatch-entry helpers. `entity_dispatch_entry_init_runtime_state` copies the shared owner byte into new runtime-state entries and re-raises the owner's `+0x40` byte when needed, which matches propagation of borrowed hold state rather than transfer of owner identity.
Current best neutral conclusion from this pass: the shared `g_active_dispatch_entry_farptr[+0x40]` byte is a startup/display presentation hold token borrowed across the seg049 controller lane and later cleanup/handoff bodies; the seg108 `0x4f38` lane is a separate local sprite-object state stack with its own bit-`0x40` contract, not the owner of the shared active-dispatch token.
### Current batch: seg126 control-stream producer tightening and completed `0x31a2` read classes
- The higher-level seg126 control-byte producer is now tighter without breaking the conservative file-backed model. `transition_preentry_run_until_complete_or_abort` (`000c:c9f4`) still has no data/bytecode arguments and is only reached from the local wrappers at `000c:7427` and `000c:0d0d`; both wrappers only stage the surrounding presentation lane before entering the seg126 loop, and neither injects script bytes or a script pointer.
- `transition_preentry_setup_resources` (`000c:c63a`) remains the only verified source of the consumed bytes. It copies the shared mutable base path from `0x6aa:0x6ac`, composes local filenames through the slash-aware seg072 helper `0009:3600` using local buffers `0x631c`, `0x6323`, `0x632c`, and `0x6335`, opens the resulting file through the seg070 file-handle lane, allocates a buffer of the returned size, and reads the full payload into `0x6301:0x6303` before seeding `0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318`.
- Neighboring seg126 code now supports the same selector-path reading. Window `000c:b018..b03d` also reloads the same shared base path from `0x6aa:0x6ac` and composes a sibling local filename through `0009:3600` using `0x621c/0x6223`, which makes the startup/display lane look more like a family of file-selected transition assets than a local script-byte emitter.
- The upstream `0x6aa:0x6ac` question is now narrow enough to close as an earlier inherited path lane rather than an in-scope seg126 producer. Literal-address instruction search still finds no store into `0x6aa` or `0x6ac`; the seg004 parser window only mutates the first byte of the pointed buffer at `0004:0ccd` / `0004:0cd8`, while the same parser explicitly installs the sibling root `0x6ae:0x6b0` from parsed input at `0004:0d28..0d2c`. Current best read is therefore: `0x6aa:0x6ac` already points at a mutable external/default base-path buffer before the seg126 startup/display family begins composing filenames on top of it.
- The neighboring seg126 helper family sharpens that close further. Window `000c:afa5..b152` keys object field `+0x49` through local values `0`, `1`, and `4`, composes three sibling filenames from the shared stem buffer `0x621c` plus suffix buffers `0x6223`, `0x622d`, and `0x6237`, loads the selected file into object `+0x520` through seg002 `0004:0098`, then runs the same display/update chain; wrapper `000c:b153..b25f` increments or decrements `+0x49` on selected event codes and re-enters that same loader. This makes the nearby seg126 lane look like a local three-way transition-asset family selector layered on top of the inherited shared base path.
- The overlap check does not force repair yet. `analyze_function_boundaries` still reports `000c:ca1d` as a plausible function entry and `000c:cd53` as the next clean function, while the oversized overlap rooted at `000c:db68` still pollutes the namespace. That overlap no longer blocks byte-behavior or read-site classification, but it still blocks clean function recovery for `transition_preentry_step_script`.
- The in-scope `0x31a2` readers are now classed cleanly by role. `0004:c24d` and `000c:e4d8` are edge waits; `000c:ca11` is the seg126 modal-break exit; `000c:e546`, `000c:e5c6`, and `000d:c0ee` are cleanup-abort exits; `000d:9304` and `000d:b6b1` are deferred dispatch/state-advance gates.
- Two remaining `0x31a2` reads stay outside that presentation classification set. `0005:453d` is only a plain getter wrapper for the shared depth word, and `0008:5149` is a seg008 internal/accounting-side read that adds the current depth to another local count before tripping a `>= 0x10` capacity flag.
### Current batch: renderer preset contract and seg127 fade-input closure
- `transition_preentry_setup_resources` is now exact on the paired renderer setup path. Instruction window `000c:c659..c6ab` shows that `FUN_000a_9748` is called only with preset ids `0x10` and `0x11`, storing the resulting temporary renderer objects at `0x8c5c:0x8c5e` and `0x8c60:0x8c62`, then immediately drawing the same seed text buffer `DS:0x631a` at `(0x0a,0x0a)` through both. This closes the structural question as `paired preset text lanes` inside one temporary transition presentation path, but still does not justify a stronger title/body or highlight/shadow label.
- The recovered `transition_preentry_step_script` body is also slightly tighter on the two text opcodes. `0x40` and `0x24` both measure their string through renderer vtable slot `+0x0c`, center it inside a `0x280`-wide lane, fetch rendered width through slot `+0x08`, draw through seg088 `000a:30d7`, blit through seg080 `0009:943a`, and advance `SI` by `rendered_width + 4`; only the selected preset lane differs (`0x8c5c` for `0x40`, `0x8c60` for `0x24`).
- The seg127 fade-controller inputs are now exact rather than only role-level. `transition_palette_fade_begin` stores palette source at `0x630e:0x6310`, start index at `0x6312`, count at `0x6314`, step at `0x6316`, brightness at `0x630d`, active flag at `0x630a`, and direction/state at `0x630b`, then immediately ticks the local fade controller. `transition_palette_fade_tick` dispatches `0x630b==1` to `transition_palette_fade_out_step` and `0x630b==2` to `transition_palette_fade_in_step`.
- The two default script-selected fade wrappers are now instruction-verified too. `palette_fade_begin_full_down` at `000c:c616` pushes direction `1`, step `4`, count `0x80`, start `0`, and palette buffer `DS:0x8c64`; `palette_fade_begin_full_up` at `000c:c600` is the same wrapper with direction `2`. Combined with the `0x5e`, `0x26`, and `0x2a` script-byte sites in `transition_preentry_step_script`, this closes the neighboring seg127 fade-input contract for the startup/display lane.
- The late presentation-handoff family is now direct-decompile confirmed rather than only caller-window inferred. `FUN_000d_938c` creates up to two temporary runtime-state palette entries (`kind 0x3c`, then `kind 0x14`), waits for them to clear, redraws, clears `g_active_dispatch_entry_farptr[+0x40]`, and only then dispatches caller vtable `+0x08`; `entity_cleanup_resources_and_dispatch` shows the same late shared-hold clear on the `entity +0x737` branch immediately before the shared `0x2bd8` controller dispatch. That is enough to treat the startup/display major section as materially complete, with only low-impact residual ambiguity around the exact UI label of preset pair `0x10/0x11` and the optional overlap hygiene at `000c:db68`.
---
## Follow-up: `0x4588` Object-Role Evidence
The `0x4588` FAR object is a runtime-installed callback/dispatch object that participates in conditional render or presentation-side flow. It has an explicit install, clear, callback, and teardown lifecycle.
### Verified lifecycle
- **Install:** `000a:4932` and `000a:4936` store the incoming dword into `0x4590` and `0x458c`, then `000a:493e` stores the incoming FAR object pointer into `0x4588`.
- **Clear:** `0004:5b8c` and `0004:5bbf` both clear `0x4588` immediately before the fatal/reporting-style seg091 call through `000a:454d`; `0004:5ea7` and `0004:6430` both clear `0x4588` and then immediately run the one-shot teardown path `000a:4a56(1)`.
- **Teardown:** `000a:4a56` checks a once-flag at `0x4595`, clears `0x4588` when non-null, optionally performs a vtable `+0x0c` callback when `0x4590 != 0x458c`, then calls vtable slot `+0x04` followed by `FUN_0009_0d30()`.
- **Callbacks:** `000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call vtable slot `+0x0c`. `entity_conditional_render_dispatch` calls the same vtable slot with a single literal `0x0101` argument.
Current batch note:
- `runtime_callback_object_init_once`, `runtime_callback_object_teardown_once`, and `entity_conditional_render_dispatch` now line up even more strongly as a video or presentation-state callback lane rather than a generic allocator client. The object is installed only after BIOS video-state snapshot, teardown emits a final callback only when recorded mode/state changed, and one live caller uses the literal mode-like pair `0x0101` through the same vtable `+0x0c` slot. That is enough to keep pushing the role toward `presentation/video-state callback broker`, but still not enough for a fully behavioral subsystem rename.
### Payload pairs from payload sync callsites
- `000d:9d5e` → vtable `+0x0c` payload from object fields `+0x12d/+0x12f`
- `000d:a3b7` → vtable `+0x0c` payload from object fields `+0x74f/+0x751`
- `000a:b9e5`, `000a:ba66` → emitting only when the candidate two-word pair differs from the current pair, then mirroring that pair through `000b:1e39` using global sprite/object pointer `0x4f38/0x4f3a`
### Globals
| Address | Name |
|---------|------|
| `0x4588` | runtime FAR object pointer (nullable) |
| `0x458c` | callback sync field (compared against `0x4590` in teardown) |
| `0x4590` | paired sync field |
| `0x4594/0x4595` | state flags |
| `0x45a6` | clock/cookie global used by `assert_buffer_valid` |
| `0x39ca` | dispatch callback-table pointer |
| `0x6828` | `g_active_dispatch_entry_farptr` |
---
## Follow-up: VM Owner/Resource Loader and Owner-Loaded Class Validation
The next ScummVM-guided validation step now confirms that the sampled owner-loaded EUSECODE classes are compatible with the ScummVM indexing model even though one header detail remains open.
### Sampled class-record findings
- Using the extracted chunks plus the live raw path `000d:44df -> 000d:4c99 -> 000d:7000`, the large chunk at table offset `0x88` behaves as object `1`.
- For representative class bodies, deriving `object_index = (table_offset - 0x80) / 8`, then `class_id = object_index - 2`, and then reading object `1` at `4 + 13 * class_id` yields the expected names: `EVENT`, `NPCTRIG`, `SURCAMNS`, `JELYHACK`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`.
- This is the first direct local confirmation that the owner-loaded records match the ScummVM `object 1` name-table plus `classid + 2` body lookup at the indexing level.
### Header and event-table shape
- The loader-side count field is now tighter too. The first dword in the sampled owner-loaded class header is not the total slot count; `000d:5066` uses it as the extra-slot count beyond a fixed `0x20` base table, which is why the cached table allocation is `extra_count * 6 + 0xc0` and the refcount array is `extra_count * 2 + 0x40`.
- That reading matches the extracted class-family shapes exactly: `EVENT` keeps first dword `0x00000000`, `NPCTRIG` moves to `0x00000001`, and `ROLL_NS` to `0x00000002`, while the already-validated owner-loaded event counts remain `0x20`, `0x21`, and `0x23` respectively.
- The sampled class records do contain a stable 4-byte header field at bytes `8..11`.
- The observed values are small boundaries: `0x00d4`, `0x00da`, and `0x00e6` in the current sample set.
- Treating that dword directly as the first post-event-table offset makes the layout line up cleanly: `(dword_at_8 - 20) / 6` yields valid tables of 32, 33, or 35 slots before inline payload/name data begins.
- The region at `class + 0x14` is therefore now directly confirmed as repeated 6-byte slots with `u16 unknown_word + u32 code_or_payload_field` layout.
- Representative low-slot examples are `JELYHACK` slot `1` = `{word=0x002a, dword=0x00000001}`, `SURCAMNS` slot `1` = `{word=0x0051, dword=0x000000d2}`, `SURCAMEW` slot `1` = `{word=0x00f7, dword=0x000000d2}`, `EVENT` slot `10` = `{word=0x1fd6, dword=0x00000001}`, and `REE_BOOT` slots `10/15/16` = `{0x034b,1}`, `{0x025c,0x034c}`, `{0x003b,0x05a8}`.
- The runtime-side selector arithmetic is now exact as well: the owner-resource callbacks operate on `class_id + 2`, which matches the extracted `object_index` column directly. `EVENT` therefore lands on child `0x363` from class id `0x361`, and `NPCTRIG` on child `0x365` from class id `0x363`.
- The leading event word is still not decoded semantically.
### What remains open
- Scanning with the previously noted ScummVM-style `(base_offset + 19) / 6` interpretation overruns into inline payload/name bytes on these owner-loaded records, so the local sample set does not support that exact event-count formula as written.
- The best current arithmetic fit is now tighter: ScummVM's decremented `base_offset` is also used as the live code-stream base in `uc_machine.cpp`, so the local owner-loaded records fit best if bytes `8..11` are the first code-byte offset and event-count derivation is `(base_offset - 19) / 6`, which is exactly equivalent here to `(raw_u32_at_8_11 - 20) / 6`.
- Current `000d` loader evidence does not point to a header rewrite before VM consumption. `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) only builds the external path and creates the runtime, `entity_vm_runtime_create` (`000d:4c99`) only installs the helper returned by `000d:7000`, `entity_vm_runtime_owner_resource_create` (`000d:7000`) only allocates the child owner table and fills it through helper vtable `+0x0c`, and `entity_vm_context_create_from_slot_index` (`000d:46ec`) directly reads slot-backed source data from that owner table. No local step is yet verified as rewriting the sampled class headers.
- The slot-value miss path is now exact enough to align against the extractor rather than only against motifs. `entity_vm_slot_load_value` (`000d:51fd`) does not build the returned workspace out of owner-row fields or late interpreter scratch: on a miss it uses `000d:5066` plus the same owner-resource wrapper `000d:714c` to read a `0x14`-byte class header, then a cached `6 * (0x20 + extra_count)` subentry table, and finally the selected subentry's byte range straight into a newly allocated value-object buffer at `+0x0a/+0x0c`.
- The final body read at `000d:53b4` now matches the extracted row arithmetic exactly. The 6-byte row contributes `word body_len` plus `dword raw_code_offset`, the class header contributes `dword code_base`, and the reader fetches `body_len` bytes from `code_base + raw_code_offset - 1` through `code_base + raw_code_offset + body_len - 2`.
- That gives a direct owner-loaded fit for the two surviving `NPCTRIG` bodies. For class `NPCTRIG` (`class_id = 0x363`, `object_index = 0x365`), slot `0x0a` uses `{len = 0x0175, raw_code_offset = 0x00000001, code_base = 0x00da}` and therefore materializes range `0x00da..0x024e` (`373` bytes), while slot `0x20` uses `{len = 0x0159, raw_code_offset = 0x00000176, code_base = 0x00da}` and therefore materializes range `0x024f..0x03a7` (`345` bytes). `EVENT` slot `0x0a` fits the same runtime arithmetic with `{len = 0x1fd6, raw_code_offset = 0x00000001, code_base = 0x00d4}` -> `0x00d4..0x20a9`.
- Because `000d:5066/51fd/53b4` now line up with the extracted class headers and event rows byte-for-byte, the remaining immortality blocker is no longer header math or slot-number translation. The open step is upstream class selection into this now-verified loader path: whether the live slot `0x0a` request really names `NPCTRIG`, `EVENT`, or another descriptor family sharing the same owner-loaded format.
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) still does not expose a direct binary-side class-name lookup or explicit `classid + 2` arithmetic. What it does expose is an indexed file-set loader contract: helper-owned count at `+0x14`, far-pointer table at `+0x10`, paired per-entry word table at `+0x18`, vtable `+0x04` size query, and vtable `+0x0c` materialization of the `0x0d`-stride owner records later consumed by `entity_vm_context_create_from_slot_index`. The current pass also makes the helper shape slightly more concrete: the two raw seg070 windows at `0009:67b6` and `0009:6916` are twin per-entry path/read loops with distinct format strings (`DS:3f2d` and `DS:3f40`) but the same `+0x10/+0x18` indexing and file open/read/close lane, which is better evidence for a multi-table or multi-phase external loader than for direct in-memory descriptor iteration.
- The signed slot-offset lane used by the still-xref-dark wrappers `0005:2c35` / `0005:2c68` is also no longer confined to `entity_vm_context_create_from_slot_index` (`000d:46ec`). Ghidra now reflects that contract in the conservative wrapper names `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset`. Inside `entity_vm_runtime_create`, the pre-entry body at `000d:4c25..4c90` reloads object fields `+0x32/+0x34` through `entity_vm_slot_load_value_plus_offset` (`000d:5572`), stores the reconstructed `DX:AX` pair into object fields `+0x10c/+0x10e`, and also caches the owner-source far pointer at `+0x117/+0x119`. The paired save path at `000d:49ec` is narrower than it first looked: it serializes only the low word at `+0x10c` through seg070 `0009:2034`, while the high word is recomputed on load from the fresh `entity_vm_slot_load_value()` result plus the saved additive word.
- Current disassembly closes the exact low-slot wrapper contracts too. `0005:2c35` sign-extends caller word `[BP+0x0a]`, then calls `entity_vm_context_try_create_masked_for_entity` with slot `0x0a` and packed mask `0x00000400`; `0005:2c68` is the same signed-additive shim for slot `0x0b` and packed mask `0x00000800`. Neither wrapper has a recovered outward code/data xref yet, so the best current provenance remains `extra-word masked materializer family member`, not a gameplay event label.
- The newly recovered post-load consumers of `+0x10c/+0x10e` are weak and do not behave like a recovered event-dispatch selector. Predicate `FUN_0001_a772` returns true only when the pair is exactly `0000:0001`, while normalization block `FUN_0002_1860` checks `segment == 0` and clamps `offset < 0x0080` up to `0x0080`. No recovered downstream comparison or dispatch branch matches the five verified `NPCTRIG` slot `0x0a` clause starts (`0x0064/0x0093/0x00c2/0x00f1/0x0120`) or backward targets (`0x001f/0x004e/0x007d/0x00ac/0x00db`); if anything, the `0x0080` floor cuts across that family instead of confirming it.
- The masked-create hub in front of that lane is now explicit too. Window `000d:463a..46e8` maps one gameplay entity through `entity_vm_slot_index_from_entity`, tests the owner/resource table row mask at `0x6611 -> +0x1315/+0x1317 -> (+0x10/+0x12) + 0x0d*slot`, and only then calls `entity_vm_context_create_from_slot_index`. That matters because the offset-specialized wrappers `0005:2c35` / `0005:2c68` are now instruction-verified as nothing more than sign-extended extra-word shims over this generic masked-context hub, rather than separate selector logic.
- The upstream slot selector is now exact enough to rule out one remaining binary-side shortcut. `entity_vm_slot_index_from_entity` (`000d:45c5`) does not expose a class-family choice like `NPCTRIG` versus `EVENT`; it only chooses one of three generic category spans before the owner row is consulted: `(a)` entity ids `1..255` with class-word bit `0x0002` clear map to `entity_id + base_0x8c7e`, `(b)` class-nibble `4` objects map to `class_byte_0x7e05 + base_0x8c80`, and `(c)` everything else maps to `type_word_0x7df9 + base_0x8c7c`.
- The runtime init path now shows where those bases come from too. After `entity_vm_runtime_create` succeeds, `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) seeds `0x8c7c/0x8c7e/0x8c80/0x8c82` as cumulative category bases by looping over four word counts at `0x6608..0x660e`. Because the compiled side only sees those category-base spans and the later owner-row mask words, it still does not reveal a direct descriptor-class discriminator before the slot body is loaded.
- One direct non-hub consumer reinforces that read. `FUN_0005_295f` is the only currently recovered caller of `entity_vm_slot_index_from_entity` outside the masked hub; it recomputes the same slot index, directly tests owner-row bit `0x0040`, and then branches into gameplay handling before optionally calling `entity_vm_context_try_create_masked_for_entity` with mask `0x0040:0x0006`. Together with the still-empty xref results for `0005:2c35` and the stable `0005:2c35..2c57` function boundary, the safest current interpretation is that these owner-row words are generic capability masks, not explicit `NPCTRIG` / `EVENT` family tags.
- The next immortality pass separates that owner-row path from the live control-stream path even more sharply. Inside `entity_vm_context_create_from_slot_index` (`000d:46ec`), the owner-table row still feeds only the preserved `0x39ca[slot]` mirror, while the actual `+0xd6/+0xd8` control stream handed to `entity_vm_context_setup` comes from `entity_vm_slot_load_value_plus_offset` and the caller-supplied setup/tail pointers come from the current VM frame record. That makes the immediate builder for the `000d:21ed` lane `slot-backed decoded stream plus frame-local replay`, not `owner-row decode`.
- That is the current hard wall for the immortality frontier. The strongest verified answer remains that `NPCTRIG` slot `0x0a` is the best upstream descriptor-side fit and `EVENT` slot `0x0a` remains the generic-hub baseline, but the binary selector path now bottoms out at category spans plus row-capability bits rather than at a provable class-family discriminator.
- The open descriptor question therefore moves one step earlier again. Current `000d` loader/runtime evidence still supports a descriptor-derived upstream workspace, but not a direct owner-row-to-opcode path for the immortality trigger. The closest verified compiled-side seeding now happens later inside the hidden dispatcher at `000c:fa2f`, where immediate literal cases can push byte/word/dword payloads straight onto the caller stream before the frame replay family re-materializes them into the child frame.
- The seg070 twin-file-family helper is now tighter at the buffer/schema level as well. The paired loops at `0009:67b6` and `0009:6916` do not reuse one ambiguous scratch object: each loop performs its own size query/allocation sequence, builds paths from the same `+0x10/+0x18/+0x14` table trio with its own format string (`DS:3f2d` versus `DS:3f40`), feeds a dedicated temporary far buffer through the shared `file_handle_alloc_init_and_open` / `dos_file_seek` / `dos_file_close` trailer, and then frees that loop-local buffer before returning. Current safest read is therefore `two distinct temporary file-family materialization passes inside one owner-resource helper`, not one callback shard reused for both families.
- Additional `0x39ca` consumers are now classified more cleanly. Beyond the already-known static seeds at `000d:7299 -> DS:67f2` and `000d:761c -> DS:6872`, the constructor-like windows at `000d:929a` and `000d:963c` seed rows `DS:68ec` and `DS:68f5` respectively before enabling local timer/dispatch behavior. Those writes behave like dispatch-entry-local static seed rows, not owner-table mirrors. Separately, `FUN_000d_938c` reads temporary dispatch-entry fields `+0x32/+0x34` at `000d:9449..9468` and `000d:9547..9566` only as a wait/poll condition on the scratch-palette (`kind 0x3c`) and current-palette (`kind 0x14`) entries it creates, which further separates active dispatch-entry state from the owner-backed `0x39ca[slot] = {source_off, source_seg}` rows written by `000d:46ec`.
- Safe event-label correlation remains intentionally narrow after this pass. The sampled low slot ids are now concrete, but none of them yet have a verified binary-side behavior match strong enough to promote a ScummVM label like `look`, `use`, or `cachein`.
### Current batch: higher-slot masked wrapper ladder (`0x10..0x14`)
- The gameplay-side masked-wrapper island now extends one verified step past the older `0x0f` frontier. Raw call setup around `0005:3115..322d` shows five higher-slot entries feeding `entity_vm_context_try_create_masked_for_entity` with slot ids `0x10`, `0x11`, `0x12`, `0x13`, and `0x14`.
- The slot `0x10` lane is not yet a clean standalone function object, but the containing body at `0005:3115..3129` is exact enough to classify its call shape: it pushes zero extra word, slot `0x10`, packed mask `0x00010000`, and the live entity pointer before the far call to `000d:463a`. The preceding guard at `0005:30f2..3113` restricts that path to one class-nibble-`4` lane.
- Four neighboring helpers are now renamed directly in Ghidra from stable function objects:
- `0005:313e` = `entity_vm_context_try_create_mask_00020000_slot11_with_offset`
- `0005:3171` = `entity_vm_context_try_create_mask_00040000_slot12`
- `0005:31da` = `entity_vm_context_try_create_mask_00080000_slot13_with_offset_if_valid_entity`
- `0005:31a0` = `entity_vm_context_try_create_mask_00100000_slot14_with_offset`
- Their payload shapes are now exact from disassembly, not only inferred from decompile:
- slot `0x11` pushes one caller-supplied extra word (`MOVZX EAX,[BP+0xa] ; PUSH EAX`)
- slot `0x12` pushes a fixed zero extra word
- slot `0x13` pushes one sign-extended caller word after the same `0005:2686` / `0005:ffed` entity-validity gate used by the older slot-`0x01` helper
- slot `0x14` pushes one caller-supplied extra word
- This widens the verified owner-slot taxonomy in a USECODE-relevant way: the binary is no longer only distinguishing compact low-slot wrappers like `0x0a`/`0x0b`; it also separates a higher-slot family with mixed `no extra word` versus `signed extra word` call contracts.
- The first outward callers in this higher-slot family are now explicit too. `entity_vm_context_try_create_mask_00040000_slot12` (`0005:3171`) is called at `0005:1776` and `0005:1945`; both callsites are currently trapped in non-function windows, but they are real direct edges into the slot-`0x12` zero-extra-word lane. By contrast, current MCP xrefs still show no direct outward callers for the slot `0x11`, `0x13`, or `0x14` wrappers and still none for the dark slot `0x0a` / `0x0b` pair.
- The persisted-context side of the same lane is now tighter at the field level. `entity_vm_context_save` (`000d:498f`) serializes `+0x11f`, `+0x121`, the derived low word at `+0x10c`, the additive word at `+0x34`, and the `0x80`-byte local buffer at `+0x36/+0x38`; `entity_vm_context_load` (`000d:4a78`) rebuilds the frame pointers, reloads the saved low word as the additive argument to `entity_vm_slot_load_value_plus_offset`, restores `+0x10c/+0x10e`, and refreshes the owner-linked source pair at `+0x117/+0x119`. That strengthens the current read that persistence preserves `(slot, additive_word, derived_low_word)` after selector choice, not the upstream class-family selector itself.
- The external event-name correlation can now be tightened slightly but still stays hint-level only:
- slot `0x12` having no extra word is compatible with the external `justMoved()` zero-argument event label
- slot `0x13` carrying one extra word is compatible with Pentagram's `AvatarStoleSomething(uword)` signature
- slot `0x11` carrying one extra word is compatible with Pentagram's placeholder `func11(sint16)` signature and with ScummVM's unresolved `cast`-side slot only at the broad `one scalar argument` level
- slot `0x14` currently does **not** fit Pentagram's older zero-argument `animGetHit()` signature, so that ordinal should remain slot-numbered on the binary side for now
- Operational consequence for the current VM lane: there is now stronger binary evidence that the masked-context family is organized around slot ordinals with distinct payload shapes, not only around one low-slot trigger subset. That helps the current round-trip IR because it justifies keeping higher ordinals as slot-stable records with payload-shape metadata even when their event labels remain external hints.
- The sequencer-side consumer model is also now preserved directly in Ghidra. Address `000d:22bc` carries a decompiler comment recording it as a sequencer-internal matrix stage: it reads two signed metadata bytes from `+0xd6/+0xd8`, consumes caller-stream words as entity/link ids, repeatedly calls `0008:7d27`, and only pushes back words without bit `0x0400` before jumping to `entity_vm_opcode_finish`.
### Conservative parser rule from this batch
- For current owner-loaded/raw EUSECODE work, keep bytes `8..11` raw and derive event count only with `(raw_u32_at_8_11 - 20) / 6` when divisibility and object-size bounds checks succeed.
- Keep the decremented `code_base_minus_one = raw_u32_at_8_11 - 1` as a separate code-addressing field rather than collapsing it into the event-count rule.
- Preserve the 6-byte event rows and their leading word verbatim until the event-entry word semantics are verified.