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:
parent
c52ce14a07
commit
71604331cf
13 changed files with 6410 additions and 1894 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue