# 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` | `FUN_0004_60c0` | Startup/display orchestration path: broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot `+0x0c`, drives the sprite/object lane through `0x4f38`, runs the seg137 palette and dispatch-entry helper family, 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 the still-unrecovered `0004:1e00` routine. | | `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 | --- ## 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. ### 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` |