diag(render): #105 round 3 — finalize-replace + late-register tripwires on EnvCellRenderer

Live evidence narrowed #105 to the pending->committed instance hand-off:
walls missing from BOTH inside and outside views while the same cells'' props
draw and collision works = the wall-shell INSTANCES never reach the committed
draw set. FinalizeLandblock uses REPLACE semantics (lb.Instances =
lb.PendingInstances), and with two-tier streaming a landblock can finalize
while a promote job is still registering its cells on the worker thread —
the partial pending set commits, the remainder lands in a fresh pending list,
and the next finalize REPLACES the committed set with only the remainder.
Whoever registered first is silently lost: per-session-random (drain timing),
per-building-persistent, from session start.

- [finalize-replace] a finalize that DISCARDS already-committed instances
- [late-register]    a RegisterCell landing after its landblock finalized

Both print only on the suspect interleavings. Next occurrence proves or
kills the theory; the fix (merge semantics + registration/finalize atomicity)
follows the evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-09 22:07:43 +02:00
parent cbba71f8a9
commit 0a38d934fd

View file

@ -349,6 +349,12 @@ public sealed unsafe class EnvCellRenderer : IDisposable
lock (lb.Lock)
{
// TEMP diagnostic #105 (strip with fix): a registration landing AFTER
// this landblock was already finalized starts a fresh pending list that
// only commits if ANOTHER finalize arrives — and that one will REPLACE
// (not merge) the committed set. One-shot per landblock per pending list.
if (lb.InstancesReady && lb.PendingInstances is null)
Console.WriteLine($"[late-register] lb=0x{landblockId:X8} cell=0x{envCellId:X8} registered AFTER finalize — starting a new pending list ({(lb.Instances?.Count ?? 0)} already committed)");
lb.PendingInstances ??= new List<EnvCellSceneryInstance>(capacity: 32);
lb.PendingInstances.Add(cellInstance);
lb.PendingEnvCellBounds ??= new Dictionary<uint, WbBoundingBox>();
@ -403,6 +409,13 @@ public sealed unsafe class EnvCellRenderer : IDisposable
{
if (lb.PendingInstances is not null)
{
// TEMP diagnostic #105 (strip with fix): REPLACE semantics — if a
// previous finalize already committed instances for this landblock,
// this swap DISCARDS them in favor of the new pending set. A partial
// pending set (finalize racing a still-registering promote job)
// silently loses buildings.
if (lb.Instances is { Count: > 0 })
Console.WriteLine($"[finalize-replace] lb=0x{landblockId:X8} DISCARDING {lb.Instances.Count} committed instances, replacing with {lb.PendingInstances.Count} pending");
lb.Instances = lb.PendingInstances;
lb.PendingInstances = null;
}