perf(streaming): cursor publication across frame budgets

This commit is contained in:
Erik 2026-07-24 19:10:18 +02:00
parent bb16f74fd4
commit 98f1ac8934
32 changed files with 2215 additions and 823 deletions

View file

@ -13,26 +13,46 @@ namespace AcDream.App.Streaming;
/// </summary>
public sealed class LandblockStaticPresentationPublication
{
private readonly IReadOnlyDictionary<uint, WorldEntity> _entities;
private readonly IReadOnlyDictionary<uint, WorldEntitySnapshot> _snapshots;
private readonly Dictionary<uint, WorldEntity> _entities;
private readonly Dictionary<uint, WorldEntitySnapshot> _snapshots;
private readonly uint[] _orderedPreviouslyActiveIds;
private readonly Dictionary<uint, WorldEntitySnapshot> _replacementActive;
internal LandblockStaticPresentationPublication(
object owner,
LandblockPhysicsPublication physicsPublication,
Dictionary<uint, WorldEntity> entities,
Dictionary<uint, WorldEntitySnapshot> snapshots,
HashSet<uint> previouslyActiveIds)
IReadOnlyDictionary<uint, WorldEntitySnapshot> priorActive,
HashSet<uint> previouslyActiveIds,
uint[] orderedPreviouslyActiveIds)
{
Owner = owner;
PhysicsPublication = physicsPublication;
_entities = entities;
_snapshots = snapshots;
PriorActive = priorActive;
PreviouslyActiveIds = previouslyActiveIds;
_orderedPreviouslyActiveIds = orderedPreviouslyActiveIds;
_replacementActive = new Dictionary<uint, WorldEntitySnapshot>(
physicsPublication.Build.Landblock.Entities.Count);
}
internal object Owner { get; }
internal LandblockPhysicsPublication PhysicsPublication { get; }
internal IReadOnlyDictionary<uint, WorldEntitySnapshot> PriorActive { get; }
internal Dictionary<uint, WorldEntity> MutableEntities => _entities;
internal Dictionary<uint, WorldEntitySnapshot> MutableSnapshots => _snapshots;
internal HashSet<uint> PreviouslyActiveIds { get; }
internal IReadOnlyList<uint> OrderedPreviouslyActiveIds =>
_orderedPreviouslyActiveIds;
internal IReadOnlyList<KeyValuePair<uint, WorldEntitySnapshot>>
OrderedSnapshots { get; set; } =
Array.Empty<KeyValuePair<uint, WorldEntitySnapshot>>();
internal Dictionary<uint, WorldEntitySnapshot> ReplacementActive =>
_replacementActive;
internal int PreparationCursor { get; set; }
internal bool PreparationCommitted { get; set; }
internal int PriorCleanupCursor { get; set; }
internal int PluginCursor { get; set; }
internal bool BeginCommitted { get; set; }
@ -116,6 +136,22 @@ public sealed class LandblockStaticPresentationPublisher
public LandblockStaticPresentationPublication PreparePublication(
LandblockPhysicsPublication physicsPublication)
{
LandblockStaticPresentationPublication publication =
CreatePublication(physicsPublication);
while (!AdvancePreparationOne(publication))
{
}
return publication;
}
/// <summary>
/// Captures prior ownership without validating the replacement entity
/// list. The concrete pipeline advances replacement validation one entity
/// at a time before any render/physics owner mutates external state.
/// </summary>
internal LandblockStaticPresentationPublication CreatePublication(
LandblockPhysicsPublication physicsPublication)
{
ArgumentNullException.ThrowIfNull(physicsPublication);
@ -125,11 +161,43 @@ public sealed class LandblockStaticPresentationPublisher
out Dictionary<uint, WorldEntitySnapshot>? active);
var entities = new Dictionary<uint, WorldEntity>();
var snapshots = new Dictionary<uint, WorldEntitySnapshot>();
foreach (WorldEntity entity in physicsPublication.Build.Landblock.Entities)
HashSet<uint> previous = active is not null
? active.Keys.ToHashSet()
: new HashSet<uint>();
uint[] orderedPrevious = previous.Order().ToArray();
return new LandblockStaticPresentationPublication(
_receiptOwner,
physicsPublication,
entities,
snapshots,
active ?? new Dictionary<uint, WorldEntitySnapshot>(),
previous,
orderedPrevious);
}
/// <summary>
/// Validates and snapshots one replacement entity. The final stable plugin
/// order is materialized only after the cursor reaches the immutable tail.
/// </summary>
internal bool AdvancePreparationOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.PreparationCommitted)
return true;
IReadOnlyList<WorldEntity> source =
publication.PhysicsPublication.Build.Landblock.Entities;
uint canonical = Canonicalize(publication.LandblockId);
if (publication.PreparationCursor < source.Count)
{
WorldEntity entity = source[publication.PreparationCursor];
if (entity.ServerGuid != 0)
continue;
if (!entities.TryAdd(entity.Id, entity))
{
publication.PreparationCursor++;
return false;
}
if (publication.Entities.ContainsKey(entity.Id))
{
throw new InvalidOperationException(
$"Landblock 0x{canonical:X8} contains duplicate DAT-static ID " +
@ -142,8 +210,9 @@ public sealed class LandblockStaticPresentationPublisher
$"DAT-static ID 0x{entity.Id:X8} is already owned by " +
$"landblock 0x{owner:X8}.");
}
if (active is not null
&& active.TryGetValue(entity.Id, out WorldEntitySnapshot retained)
if (publication.PriorActive.TryGetValue(
entity.Id,
out WorldEntitySnapshot retained)
&& retained.SourceId != entity.SourceGfxObjOrSetupId)
{
throw new InvalidOperationException(
@ -152,18 +221,17 @@ public sealed class LandblockStaticPresentationPublisher
$"0x{entity.SourceGfxObjOrSetupId:X8}.");
}
snapshots.Add(entity.Id, Snapshot(entity));
publication.MutableEntities.Add(entity.Id, entity);
publication.MutableSnapshots.Add(entity.Id, Snapshot(entity));
publication.PreparationCursor++;
return false;
}
HashSet<uint> previous = active is not null
? active.Keys.ToHashSet()
: new HashSet<uint>();
return new LandblockStaticPresentationPublication(
_receiptOwner,
physicsPublication,
entities,
snapshots,
previous);
publication.OrderedSnapshots = publication.Snapshots
.OrderBy(static pair => pair.Key)
.ToArray();
publication.PreparationCommitted = true;
return true;
}
/// <summary>
@ -174,13 +242,31 @@ public sealed class LandblockStaticPresentationPublisher
public void BeginPublication(LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
uint[] priorIds = publication.PreviouslyActiveIds.Order().ToArray();
while (publication.PriorCleanupCursor < priorIds.Length)
if (!publication.PreparationCommitted)
throw new InvalidOperationException(
"Static presentation cannot begin before preparation commits.");
while (!AdvanceBeginOne(publication))
{
uint id = priorIds[publication.PriorCleanupCursor];
}
}
/// <summary>
/// Advances exactly one retained prior-owner cleanup. The cursor advances
/// only after the complete owner operation succeeds, so a failure retries
/// the same ID without rebuilding or reordering the work list.
/// </summary>
internal bool AdvanceBeginOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return true;
if (publication.PriorCleanupCursor
< publication.OrderedPreviouslyActiveIds.Count)
{
uint id = publication.OrderedPreviouslyActiveIds[
publication.PriorCleanupCursor];
bool retained = publication.Entities.ContainsKey(id);
_lighting.UnregisterOwner(id, forgetState: !retained);
if (!retained)
@ -192,10 +278,12 @@ public sealed class LandblockStaticPresentationPublisher
_pluginRemovalCount++;
}
publication.PriorCleanupCursor++;
return false;
}
publication.BeginCommitted = true;
_beginCount++;
return true;
}
/// <summary>
@ -258,15 +346,32 @@ public sealed class LandblockStaticPresentationPublisher
throw new InvalidOperationException(
"Static presentation requires completed physics publication.");
}
if (publication.CompletionCommitted)
return;
KeyValuePair<uint, WorldEntitySnapshot>[] snapshots = publication.Snapshots
.OrderBy(pair => pair.Key)
.ToArray();
while (publication.PluginCursor < snapshots.Length)
while (!AdvanceCompleteOne(publication))
{
(uint id, WorldEntitySnapshot snapshot) = snapshots[publication.PluginCursor];
}
}
/// <summary>
/// Advances exactly one plugin projection, then atomically publishes the
/// retained replacement dictionary after the cursor reaches its tail.
/// </summary>
internal bool AdvanceCompleteOne(
LandblockStaticPresentationPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted
|| !publication.PhysicsPublication.CompletionCommitted)
{
throw new InvalidOperationException(
"Static presentation requires completed physics publication.");
}
if (publication.CompletionCommitted)
return true;
if (publication.PluginCursor < publication.OrderedSnapshots.Count)
{
(uint id, WorldEntitySnapshot snapshot) =
publication.OrderedSnapshots[publication.PluginCursor];
_worldState.Add(snapshot);
if (publication.PreviouslyActiveIds.Contains(id))
{
@ -279,21 +384,19 @@ public sealed class LandblockStaticPresentationPublisher
_pluginSpawnCount++;
}
_landblockByEntityId[id] = Canonicalize(publication.LandblockId);
publication.ReplacementActive[id] = snapshot;
publication.PluginCursor++;
return false;
}
uint canonical = Canonicalize(publication.LandblockId);
if (publication.Snapshots.Count == 0)
{
if (publication.ReplacementActive.Count == 0)
_activeByLandblock.Remove(canonical);
}
else
{
_activeByLandblock[canonical] = publication.Snapshots
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
_activeByLandblock[canonical] = publication.ReplacementActive;
publication.CompletionCommitted = true;
_completeCount++;
return true;
}
public void RemoveLighting(WorldEntity entity)