# 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 `_all` registry; - 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_lights` `0x0052D410` - `Render::insert_light` `0x0054D1B0` - `Render::add_static_light` `0x0054D3E0` - `Render::add_dynamic_light` `0x0054D420` - 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`: ```text 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: ```text 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: ```text (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. ```text 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.