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
|
|
@ -3,6 +3,81 @@ using AcDream.Core.World.Cells;
|
|||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Per-landblock installed-key ledger for one collision-world map. Slot lists
|
||||
/// mirror the shadow registry's prefix-owner-slot idiom: removal tombstones a
|
||||
/// slot (key 0) so an in-flight metered cursor retains its captured list
|
||||
/// reference and observes only tombstones, and an emptied container is
|
||||
/// reclaimed so a future install gets a fresh compact list. Maintained by the
|
||||
/// <see cref="CollisionWorldState"/> typed install/remove helpers; consumed by
|
||||
/// the landblock-replacement seal so capturing one prefix's keys never scans
|
||||
/// the whole resident world (O1 of the 2026-08-02 collision
|
||||
/// publication-throughput fix).
|
||||
/// </summary>
|
||||
internal sealed class PrefixKeyIndex
|
||||
{
|
||||
private readonly Dictionary<uint, List<uint>> _slots = new();
|
||||
private readonly Dictionary<uint, Dictionary<uint, int>> _indices = new();
|
||||
private readonly Dictionary<uint, Stack<int>> _freeSlots = new();
|
||||
|
||||
internal void Add(uint key)
|
||||
{
|
||||
uint prefix = key & 0xFFFF0000u;
|
||||
if (!_slots.TryGetValue(prefix, out List<uint>? slots))
|
||||
{
|
||||
slots = new List<uint>();
|
||||
_slots[prefix] = slots;
|
||||
_indices[prefix] = new Dictionary<uint, int>();
|
||||
_freeSlots[prefix] = new Stack<int>();
|
||||
}
|
||||
Dictionary<uint, int> indices = _indices[prefix];
|
||||
if (indices.ContainsKey(key))
|
||||
return;
|
||||
if (_freeSlots[prefix].TryPop(out int freeIndex))
|
||||
{
|
||||
slots[freeIndex] = key;
|
||||
indices[key] = freeIndex;
|
||||
return;
|
||||
}
|
||||
indices[key] = slots.Count;
|
||||
slots.Add(key);
|
||||
}
|
||||
|
||||
internal void Remove(uint key)
|
||||
{
|
||||
uint prefix = key & 0xFFFF0000u;
|
||||
if (!_indices.TryGetValue(prefix, out Dictionary<uint, int>? indices)
|
||||
|| !indices.Remove(key, out int slotIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_slots[prefix][slotIndex] = 0u;
|
||||
_freeSlots[prefix].Push(slotIndex);
|
||||
if (indices.Count != 0)
|
||||
return;
|
||||
// An in-flight seal cursor retains its captured List reference and
|
||||
// observes only tombstones. A future install gets a fresh container.
|
||||
_slots.Remove(prefix);
|
||||
_indices.Remove(prefix);
|
||||
_freeSlots.Remove(prefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The live slot list for one landblock prefix, or null when no key is
|
||||
/// installed. Callers capture the reference plus <c>Count</c> once and
|
||||
/// iterate by index, skipping tombstone slots (key 0).
|
||||
/// </summary>
|
||||
internal List<uint>? SlotsForPrefix(uint prefix) =>
|
||||
_slots.TryGetValue(prefix & 0xFFFF0000u, out List<uint>? slots)
|
||||
? slots
|
||||
: null;
|
||||
|
||||
internal int InstalledKeyCountForPrefix(uint prefix) =>
|
||||
_indices.TryGetValue(prefix & 0xFFFF0000u, out var indices)
|
||||
? indices.Count
|
||||
: 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One exclusive-by-ownership collision-world root. A preparation mutates only
|
||||
/// its private root; activation transfers the complete root through one volatile
|
||||
|
|
@ -38,6 +113,128 @@ internal sealed class CollisionWorldState
|
|||
internal List<uint> ShadowOwnerSlots { get; } = new();
|
||||
internal Dictionary<uint, int> ShadowOwnerIndices { get; } = new();
|
||||
internal Stack<int> ShadowOwnerFreeSlots { get; } = new();
|
||||
|
||||
// ── O1 per-prefix installed-key ledgers ────────────────────────────────
|
||||
// Every mutation of the five landblock-scoped world maps goes through the
|
||||
// typed helpers below so these ledgers stay exact. The seal's landblock-
|
||||
// replacement builders enumerate one prefix's keys instead of scanning the
|
||||
// whole resident map, and the retirement/removal paths retire one prefix
|
||||
// in O(prefix keys).
|
||||
internal PrefixKeyIndex CellStructKeys { get; } = new();
|
||||
internal PrefixKeyIndex FlatCellStructKeys { get; } = new();
|
||||
internal PrefixKeyIndex FlatEnvCellKeys { get; } = new();
|
||||
internal PrefixKeyIndex BuildingKeys { get; } = new();
|
||||
internal PrefixKeyIndex EnvCellKeys { get; } = new();
|
||||
|
||||
internal void SetCellStruct(uint id, CellPhysics value)
|
||||
{
|
||||
CellStruct[id] = value;
|
||||
CellStructKeys.Add(id);
|
||||
}
|
||||
|
||||
internal bool TryAddCellStruct(uint id, CellPhysics value)
|
||||
{
|
||||
if (!CellStruct.TryAdd(id, value))
|
||||
return false;
|
||||
CellStructKeys.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool RemoveCellStruct(uint id)
|
||||
{
|
||||
if (!CellStruct.TryRemove(id, out _))
|
||||
return false;
|
||||
CellStructKeys.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
|
||||
{
|
||||
FlatCellStruct[id] = value;
|
||||
FlatCellStructKeys.Add(id);
|
||||
}
|
||||
|
||||
internal bool TryAddFlatCellStruct(uint id, FlatCellStructureCollisionAsset value)
|
||||
{
|
||||
if (!FlatCellStruct.TryAdd(id, value))
|
||||
return false;
|
||||
FlatCellStructKeys.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool RemoveFlatCellStruct(uint id)
|
||||
{
|
||||
if (!FlatCellStruct.TryRemove(id, out _))
|
||||
return false;
|
||||
FlatCellStructKeys.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetFlatEnvCell(uint id, FlatEnvCellTopology value)
|
||||
{
|
||||
FlatEnvCell[id] = value;
|
||||
FlatEnvCellKeys.Add(id);
|
||||
}
|
||||
|
||||
internal bool TryAddFlatEnvCell(uint id, FlatEnvCellTopology value)
|
||||
{
|
||||
if (!FlatEnvCell.TryAdd(id, value))
|
||||
return false;
|
||||
FlatEnvCellKeys.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool RemoveFlatEnvCell(uint id)
|
||||
{
|
||||
if (!FlatEnvCell.TryRemove(id, out _))
|
||||
return false;
|
||||
FlatEnvCellKeys.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetBuilding(uint id, BuildingPhysics value)
|
||||
{
|
||||
Buildings[id] = value;
|
||||
BuildingKeys.Add(id);
|
||||
}
|
||||
|
||||
internal bool TryAddBuilding(uint id, BuildingPhysics value)
|
||||
{
|
||||
if (!Buildings.TryAdd(id, value))
|
||||
return false;
|
||||
BuildingKeys.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool RemoveBuilding(uint id)
|
||||
{
|
||||
if (!Buildings.TryRemove(id, out _))
|
||||
return false;
|
||||
BuildingKeys.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetEnvCell(uint id, EnvCell value)
|
||||
{
|
||||
EnvCells[id] = value;
|
||||
EnvCellKeys.Add(id);
|
||||
}
|
||||
|
||||
internal bool TryAddEnvCell(uint id, EnvCell value)
|
||||
{
|
||||
if (!EnvCells.TryAdd(id, value))
|
||||
return false;
|
||||
EnvCellKeys.Add(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool RemoveEnvCell(uint id)
|
||||
{
|
||||
if (!EnvCells.TryRemove(id, out _))
|
||||
return false;
|
||||
EnvCellKeys.Remove(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -83,5 +280,18 @@ internal sealed class CollisionWorldStateSlot
|
|||
return transferred;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// O2 (2026-08-02): terminally revokes a consumed staging root. The
|
||||
/// per-landblock delta commit installs the staged content into the active
|
||||
/// root instead of swapping roots, so the staging root no longer becomes
|
||||
/// the active root — but a committed preparation must still lose access to
|
||||
/// its private world exactly as the old transfer revoked it.
|
||||
/// </summary>
|
||||
internal void Revoke()
|
||||
{
|
||||
_revoked = true;
|
||||
_current = null;
|
||||
}
|
||||
|
||||
internal CollisionWorldState Capture() => Current;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue