wip(physics): collision O(changed) delta-commit (O1-O3) - ON HOLD, feel-test failed

Publication-throughput rework per the D2 design (docs/research/
2026-08-02-collision-throughput-handoff/design-note.md): O1 per-prefix
installed-key ledgers replacing the seal's full-map scans; O2 per-
landblock delta commit (LandblockReplacementApplyCursor against the
active root) replacing whole-world TransferTo; O3 empty staging root,
commit-time reflood (CObjCell::init_objects 0x0052B420 ->
recalc_cross_cells 0x00515A30), journal/peer-rebase machinery deleted
(~1,900 lines net).

Automated gates green: Runtime 999, Core physics 2,135, App 4,039/3,
Headless 79, complete solution 10,812/0/4; lifecycle gate PASS
(connected-world-gate-20260802-193029). Soak 194423: publication-side
acceptance fully met (37 -> 4 failures, all convergence dims zero,
loadedLandblocks baseline-identical, waitCue 6/9 -> 1/9).

COMMITTED AS WIP ON USER DIRECTION - NOT ACCEPTED. The user feel-test
FAILED on this tree: monsters still pop into existence at close range,
monsters spawned mid-air far ahead, static placements visibly wrong,
plus 243x "Landblock already has a full retirement receipt"
InvalidOperationException catch-retry loop during origin recenter
(launch-feeltest-oclone.log). The 4 remaining soak failures
(pendingLandblockRetirements 131/122 at the Caul->Sawato stops) and the
implementer's "exposed pre-existing" classification are under
re-judgment against that loop. Dual reviews were dispatched and then
stopped mid-flight on user direction; NO review has passed this commit.
Full problem inventory + next-agent instructions:
docs/research/2026-08-02-collision-throughput-handoff/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-02 20:06:59 +02:00
parent c52ce14a07
commit 71604331cf
13 changed files with 6410 additions and 1894 deletions

View file

@ -483,9 +483,9 @@ public sealed class PhysicsDataCache
preparedTopology = null;
}
if (preparedStructure is not null)
_flatCellStruct.TryAdd(envCellId, preparedStructure);
_collisionWorld.Current.TryAddFlatCellStruct(envCellId, preparedStructure);
if (preparedTopology is not null)
_flatEnvCell.TryAdd(envCellId, preparedTopology);
_collisionWorld.Current.TryAddFlatEnvCell(envCellId, preparedTopology);
// UCG Stage 1: register only a loadable authored cell.
if (!CellGraph.Contains(envCellId))
@ -558,7 +558,7 @@ public sealed class PhysicsDataCache
// for every ordinary (non-house-barrier) cell.
RestrictionObj = envCell.RestrictionObj,
};
_cellStruct[envCellId] = cellPhysics;
_collisionWorld.Current.SetCellStruct(envCellId, cellPhysics);
if (PhysicsDiagnostics.ProbeDumpCellsEnabled
&& PhysicsDiagnostics.ProbeDumpCellIds.Contains(envCellId))
@ -696,8 +696,8 @@ public sealed class PhysicsDataCache
if (preparedStructure.ContainmentBsp.RootIndex < 0)
return;
_flatCellStruct.TryAdd(envCellId, preparedStructure);
_flatEnvCell.TryAdd(envCellId, preparedTopology);
_collisionWorld.Current.TryAddFlatCellStruct(envCellId, preparedStructure);
_collisionWorld.Current.TryAddFlatEnvCell(envCellId, preparedTopology);
if (!CellGraph.Contains(envCellId))
{
@ -724,7 +724,7 @@ public sealed class PhysicsDataCache
portal.Flags));
}
_cellStruct.TryAdd(envCellId, new CellPhysics
_collisionWorld.Current.TryAddCellStruct(envCellId, new CellPhysics
{
SourceId = envCellId,
WorldTransform = worldTransform,
@ -914,7 +914,7 @@ public sealed class PhysicsDataCache
/// dat-driven <see cref="CacheCellStruct"/>.
/// </summary>
public void RegisterCellStructForTest(uint envCellId, CellPhysics physics)
=> _cellStruct[envCellId] = physics;
=> _collisionWorld.Current.SetCellStruct(envCellId, physics);
/// <summary>
/// Indoor walking Phase 2 (2026-05-19). Cache the building portal list
@ -926,7 +926,7 @@ public sealed class PhysicsDataCache
{
if (_buildings.ContainsKey(landcellId)) return;
Matrix4x4.Invert(worldTransform, out var inverse);
_buildings[landcellId] = new BuildingPhysics
_collisionWorld.Current.SetBuilding(landcellId, new BuildingPhysics
{
WorldTransform = worldTransform,
InverseWorldTransform = inverse,
@ -935,7 +935,7 @@ public sealed class PhysicsDataCache
// (0x00534030) — and one building per origin landcell mirrors
// CLandBlock::init_buildings (0x0052fd80).
ModelId = modelId,
};
});
}
/// <summary>
@ -954,10 +954,33 @@ public sealed class PhysicsDataCache
/// </summary>
public void RemoveBuildingsForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
foreach (var key in _buildings.Keys)
if ((key & 0xFFFF0000u) == prefix)
_buildings.TryRemove(key, out _);
CollisionWorldState world = _collisionWorld.Current;
RemovePrefixKeys(
world.BuildingKeys,
landblockId & 0xFFFF0000u,
world.RemoveBuilding);
}
/// <summary>
/// Retires one landblock prefix's installed keys through the O1 ledger:
/// O(prefix keys), never a whole-map scan. Removal tombstones the captured
/// slot list, so index iteration over the captured reference stays exact.
/// </summary>
private static void RemovePrefixKeys(
PrefixKeyIndex ledger,
uint prefix,
Func<uint, bool> remove)
{
List<uint>? slots = ledger.SlotsForPrefix(prefix);
if (slots is null)
return;
int limit = slots.Count;
for (int index = 0; index < limit; index++)
{
uint key = slots[index];
if (key != 0u)
remove(key);
}
}
/// <summary>
@ -973,15 +996,13 @@ public sealed class PhysicsDataCache
public void RemoveCellsForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
foreach (var key in _cellStruct.Keys)
if ((key & 0xFFFF0000u) == prefix)
_cellStruct.TryRemove(key, out _);
foreach (var key in _flatCellStruct.Keys)
if ((key & 0xFFFF0000u) == prefix)
_flatCellStruct.TryRemove(key, out _);
foreach (var key in _flatEnvCell.Keys)
if ((key & 0xFFFF0000u) == prefix)
_flatEnvCell.TryRemove(key, out _);
CollisionWorldState world = _collisionWorld.Current;
RemovePrefixKeys(world.CellStructKeys, prefix, world.RemoveCellStruct);
RemovePrefixKeys(
world.FlatCellStructKeys,
prefix,
world.RemoveFlatCellStruct);
RemovePrefixKeys(world.FlatEnvCellKeys, prefix, world.RemoveFlatEnvCell);
}
public BuildingPhysics? GetBuilding(uint landcellId)
@ -990,7 +1011,8 @@ public sealed class PhysicsDataCache
public IReadOnlyCollection<uint> BuildingIds => (IReadOnlyCollection<uint>)_buildings.Keys;
/// <summary>Test helper, mirrors <see cref="RegisterCellStructForTest"/>.</summary>
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) => _buildings[landcellId] = b;
public void RegisterBuildingForTest(uint landcellId, BuildingPhysics b) =>
_collisionWorld.Current.SetBuilding(landcellId, b);
internal sealed class LandblockReplacementBuilder : IDisposable
{
@ -1017,10 +1039,9 @@ public sealed class PhysicsDataCache
private readonly List<uint> _removeFlatEnvCells = new();
private readonly List<uint> _removeBuildings = new();
private readonly UcgCellGraph.LandblockReplacementBuilder _cellGraph;
private IEnumerator<KeyValuePair<uint, CellPhysics>>? _cellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatCellStructureCollisionAsset>>? _flatCellEnumerator;
private IEnumerator<KeyValuePair<uint, FlatEnvCellTopology>>? _flatEnvEnumerator;
private IEnumerator<KeyValuePair<uint, BuildingPhysics>>? _buildingEnumerator;
private List<uint>? _keySlots;
private int _keySlotLimit;
private bool _keySlotsCaptured;
private int _phase;
private int _cursor;
@ -1078,93 +1099,139 @@ public sealed class PhysicsDataCache
_phase++;
return false;
case 2:
_cellEnumerator ??= _staging._cellStruct.GetEnumerator();
if (CapturePrefixOne(_cellEnumerator, _prefix, _cells, _cellIds))
{
// O1: enumerate the staging root's installed target-prefix
// keys instead of scanning the whole staging map, one key
// per advance.
if (TryTakeNextPrefixKey(
StagingWorld.CellStructKeys,
out uint id))
{
CaptureInstall(_staging._cellStruct, id, _cells, _cellIds);
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
}
case 3:
_cellEnumerator ??= _active._cellStruct.GetEnumerator();
if (CaptureRemovalOne(_cellEnumerator, _prefix, _cellIds, _removeCells))
{
// O1: enumerate the active root's installed target-prefix
// keys for removal capture. This also removes the previous
// cross-frame live enumerator over the active map.
if (TryTakeNextPrefixKey(
ActiveWorld.CellStructKeys,
out uint id))
{
CaptureRemoval(_active._cellStruct, id, _cellIds, _removeCells);
WorkUnits++;
return false;
}
_cellEnumerator.Dispose();
_cellEnumerator = null;
_phase++;
return false;
}
case 4:
_flatCellEnumerator ??= _staging._flatCellStruct.GetEnumerator();
if (CapturePrefixOne(_flatCellEnumerator, _prefix, _flatCells, _flatCellIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.FlatCellStructKeys,
out uint id))
{
CaptureInstall(
_staging._flatCellStruct,
id,
_flatCells,
_flatCellIds);
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
}
case 5:
_flatCellEnumerator ??= _active._flatCellStruct.GetEnumerator();
if (CaptureRemovalOne(_flatCellEnumerator, _prefix, _flatCellIds, _removeFlatCells))
{
if (TryTakeNextPrefixKey(
ActiveWorld.FlatCellStructKeys,
out uint id))
{
CaptureRemoval(
_active._flatCellStruct,
id,
_flatCellIds,
_removeFlatCells);
WorkUnits++;
return false;
}
_flatCellEnumerator.Dispose();
_flatCellEnumerator = null;
_phase++;
return false;
}
case 6:
_flatEnvEnumerator ??= _staging._flatEnvCell.GetEnumerator();
if (CapturePrefixOne(_flatEnvEnumerator, _prefix, _flatEnvCells, _flatEnvCellIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.FlatEnvCellKeys,
out uint id))
{
CaptureInstall(
_staging._flatEnvCell,
id,
_flatEnvCells,
_flatEnvCellIds);
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
}
case 7:
_flatEnvEnumerator ??= _active._flatEnvCell.GetEnumerator();
if (CaptureRemovalOne(_flatEnvEnumerator, _prefix, _flatEnvCellIds, _removeFlatEnvCells))
{
if (TryTakeNextPrefixKey(
ActiveWorld.FlatEnvCellKeys,
out uint id))
{
CaptureRemoval(
_active._flatEnvCell,
id,
_flatEnvCellIds,
_removeFlatEnvCells);
WorkUnits++;
return false;
}
_flatEnvEnumerator.Dispose();
_flatEnvEnumerator = null;
_phase++;
return false;
}
case 8:
_buildingEnumerator ??= _staging._buildings.GetEnumerator();
if (CapturePrefixOne(_buildingEnumerator, _prefix, _buildings, _buildingIds))
{
if (TryTakeNextPrefixKey(
StagingWorld.BuildingKeys,
out uint id))
{
CaptureInstall(
_staging._buildings,
id,
_buildings,
_buildingIds);
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
}
case 9:
_buildingEnumerator ??= _active._buildings.GetEnumerator();
if (CaptureRemovalOne(_buildingEnumerator, _prefix, _buildingIds, _removeBuildings))
{
if (TryTakeNextPrefixKey(
ActiveWorld.BuildingKeys,
out uint id))
{
CaptureRemoval(
_active._buildings,
id,
_buildingIds,
_removeBuildings);
WorkUnits++;
return false;
}
_buildingEnumerator.Dispose();
_buildingEnumerator = null;
_phase++;
return false;
}
case 10:
WorkUnits++;
if (!_cellGraph.Advance())
@ -1210,43 +1277,70 @@ public sealed class PhysicsDataCache
destination.TryAdd(id, value);
}
private static bool CapturePrefixOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
private CollisionWorldState StagingWorld =>
_staging._collisionWorld.Current;
private CollisionWorldState ActiveWorld =>
_active._collisionWorld.Current;
/// <summary>
/// O1 metered prefix-key cursor. The first call of a phase captures
/// the ledger's live slot-list reference and count; later calls
/// iterate by index, skipping tombstones (key 0). Removal only ever
/// tombstones a slot, so a captured reference stays exact across
/// frames without holding a map enumerator.
/// </summary>
private bool TryTakeNextPrefixKey(PrefixKeyIndex ledger, out uint key)
{
if (!_keySlotsCaptured)
{
_keySlots = ledger.SlotsForPrefix(_prefix);
_keySlotLimit = _keySlots?.Count ?? 0;
_keySlotsCaptured = true;
_cursor = 0;
}
while (_cursor < _keySlotLimit)
{
uint candidate = _keySlots![_cursor++];
if (candidate != 0u)
{
key = candidate;
return true;
}
}
key = 0u;
_keySlots = null;
_keySlotsCaptured = false;
return false;
}
private static void CaptureInstall<T>(
ConcurrentDictionary<uint, T> source,
uint id,
List<KeyValuePair<uint, T>> destination,
HashSet<uint> ids)
{
if (!enumerator.MoveNext())
return false;
KeyValuePair<uint, T> pair = enumerator.Current;
if ((pair.Key & 0xFFFF0000u) == prefix)
if (source.TryGetValue(id, out T? value))
{
destination.Add(pair);
ids.Add(pair.Key);
destination.Add(new KeyValuePair<uint, T>(id, value));
ids.Add(id);
}
return true;
}
private static bool CaptureRemovalOne<T>(
IEnumerator<KeyValuePair<uint, T>> enumerator,
uint prefix,
private static void CaptureRemoval<T>(
ConcurrentDictionary<uint, T> source,
uint id,
HashSet<uint> retained,
List<uint> destination)
{
if (!enumerator.MoveNext())
return false;
uint id = enumerator.Current.Key;
if ((id & 0xFFFF0000u) == prefix && !retained.Contains(id))
if (!retained.Contains(id) && source.ContainsKey(id))
destination.Add(id);
return true;
}
public void Dispose()
{
_cellEnumerator?.Dispose();
_flatCellEnumerator?.Dispose();
_flatEnvEnumerator?.Dispose();
_buildingEnumerator?.Dispose();
_keySlots = null;
_keySlotsCaptured = false;
_cellGraph.Dispose();
}
}