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 |