Crusader_Decomp/docs/raw-000a-000d.md

443 lines
72 KiB
Markdown
Raw Normal View History

# 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.