Replace over-cap full sorting with a retained exact top-k heap while preserving the accepted tie-order fallback. Differential tests lock randomized and Town Network-scale output, and the measured 463-light path cuts selector CPU by 29 percent without warmed allocations.
5 KiB
Slice H-b — retail light insertion and bounded top-k
Scope
This slice changes only the overflow-selection mechanism inside acdream's existing approved point-light snapshot. It preserves:
- the resident
_allregistry; - the optional last-rendered-visible-cell candidate filter;
- dynamic-before-static priority;
- squared distance from the player;
MaxGlobalLights = 128;- final nearest-first snapshot order and stable shader indices.
It does not claim to port retail's separate 7-dynamic/40-static pools or its DBObj-resident cell lifecycle. Those known differences remain AP-85.
Retail oracle
Named source:
CEnvCell::add_dynamic_lights0x0052D410Render::insert_light0x0054D1B0Render::add_static_light0x0054D3E0Render::add_dynamic_light0x0054D420- cap globals
0x0081EC94/0x0081EC98
CEnvCell::add_dynamic_lights walks the resident
CEnvCell::visible_cell_table and submits each light in registry traversal
order. Static and dynamic callers feed separate bounded pools.
Readable pseudocode for Render::insert_light:
distanceSq = point-light distance from Render::player_pos
insertIndex = 0
while insertIndex < count
and sorted[insertIndex].distanceSq <= distanceSq:
insertIndex++
if count < capacity:
count++
else if insertIndex == count:
return # new light ranks beyond the cap
reuse the prior farthest storage record
shift sorted pointers [insertIndex .. count-2] one place right
sorted[insertIndex] = reused record
populate the reused record from LIGHTINFO
Equal-distance ordering was ambiguous in the pseudo-C because Binary Ninja
left test ah, 0x5 untranslated. The exact matching retail executable
(check_exe_pdb.py GUID
9e847e2f-777c-4bd9-886c-22256bb87f32) disassembles at
0x0054D253 as:
fld newDistanceSq
fcomp existingDistanceSq
fnstsw ax
test ah, 5
jnp break
For equality, x87 sets C3 while the mask reads C2/C0 as zero; parity is even,
so jnp is not taken and traversal continues. A later equal-distance light
therefore follows earlier residents. Retail's semantic total rank is:
(pool priority, player distance squared, qualifying registration ordinal)
The ACE server, ACViewer, Holtburger, and extracted WorldBuilder paths do not implement this retail client-side bounded render-light selection. They provide no competing algorithm; named retail plus the matching executable are the oracle.
The accepted acdream implementation used .NET List.Sort with a comparator
that returned zero for equal pool/distance ranks. List.Sort is unstable, so
its tie permutation differs from retail registration order and is observable
through snapshot/shader indices. Slice H-b is explicitly performance-only: a
tied overflow frame therefore retains that exact full-sort oracle. AP-85's
eventual dual-pool visual gate is the correct place to change tie behavior to
retail.
Modern bounded implementation
Retail's capacities are tiny enough for insertion. Acdream's adaptation keeps 128 lights, so repeated insertion would do more movement than a bounded max-heap.
snapshot = empty
heap = empty
snapshot temporarily retains all candidates, as before
ordinal = 0
overflow = false
for each registered light:
if unlit or directional or outside optional cell filter:
continue
rank = (dynamic first, squared player distance, ordinal++)
snapshot.add(light)
if snapshot.count <= cap:
continue
if first overflow:
convert the first cap snapshot items to ranked heap entries
heapify with WORST rank at root
overflow = true
if rank is better than heap.root:
heap.root = rank
sift root down
if overflow:
sort only the cap heap entries by the total rank
comparatorTie =
any equal (pool, distance) ranks within the selected heap
OR more than one complete candidate has the cutoff rank
if overflow and comparatorTie:
run the prior complete List.Sort comparator over snapshot
keep its first cap entries exactly
else if overflow:
replace snapshot contents with their lights
Complexity changes from O(N log N) to O(N log K + K log K), where
K = 128, for the ordinary no-tie path. Tie detection is post-selection:
equal ranks that are entirely below the cutoff cannot affect the submitted
snapshot and therefore do not force a fallback. Equal ranks within the
selected set or straddling its cutoff intentionally fall back to
O(N log N) so this optimization cannot alter accepted presentation.
Retained lists and cached comparison delegates make both warmed paths
allocation-free.
Mandatory equivalence
- randomized differential comparison against the accepted complete-sort comparator;
- all-static, all-dynamic, mixed, filtered, equal-distance, and cap-boundary cases;
- a deterministic 463-light Town Network scale fixture;
- identical final object references and order, not only an equal set;
- zero stable allocations after retained scratch reaches capacity.