refactor(streaming): own landblock publication transactions
This commit is contained in:
parent
4001251064
commit
4f965d0699
5 changed files with 1301 additions and 122 deletions
|
|
@ -8,6 +8,18 @@ using AcDream.Core.World;
|
|||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Exact result of one committed landblock bucket mutation. Fallible mesh-pin
|
||||
/// and static-script reconciliation consume this receipt separately, so a
|
||||
/// callback failure cannot force the spatial mutation to replay.
|
||||
/// </summary>
|
||||
internal sealed record GpuLandblockSpatialPublication(
|
||||
uint LandblockId,
|
||||
LoadedLandblock Landblock,
|
||||
IReadOnlyList<ulong> AdditionalRenderIds,
|
||||
IReadOnlyList<WorldEntity> StaticEntities,
|
||||
bool RequiresActivation = true);
|
||||
|
||||
/// <summary>
|
||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
||||
/// entities. All mutation happens in <see cref="StreamingController.Tick"/>
|
||||
|
|
@ -619,12 +631,44 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
LandblockStreamTier tier = LandblockStreamTier.Near)
|
||||
{
|
||||
using MutationBatch mutation = BeginMutationBatch();
|
||||
GpuLandblockSpatialPublication publication =
|
||||
CommitLandblockSpatialCore(landblock, additionalRenderIds, tier);
|
||||
ActivateLandblockPresentation(publication);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits only canonical buckets, indexes, pending-live merge, tier, and
|
||||
/// readiness-id ownership. The caller holds one outer
|
||||
/// <see cref="MutationBatch"/> through the subsequent
|
||||
/// <see cref="ActivateLandblockPresentation"/> call so live visibility
|
||||
/// observers retain the shipped commit boundary.
|
||||
/// </summary>
|
||||
internal GpuLandblockSpatialPublication CommitLandblockSpatial(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
LandblockStreamTier tier) =>
|
||||
CommitLandblockSpatialCore(landblock, additionalRenderIds, tier);
|
||||
|
||||
private GpuLandblockSpatialPublication CommitLandblockSpatialCore(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
LandblockStreamTier tier)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
|
||||
// A stale Far completion must never replace a newer Near entity layer.
|
||||
// StreamingController normally filters it before render-side apply;
|
||||
// keep the state boundary independently monotonic as well.
|
||||
if (tier == LandblockStreamTier.Far && IsNearTier(landblock.LandblockId))
|
||||
return;
|
||||
{
|
||||
LoadedLandblock retained = _loaded[landblock.LandblockId];
|
||||
return new GpuLandblockSpatialPublication(
|
||||
retained.LandblockId,
|
||||
retained,
|
||||
Array.Empty<ulong>(),
|
||||
Array.Empty<WorldEntity>(),
|
||||
RequiresActivation: false);
|
||||
}
|
||||
|
||||
// If pending live entities have been waiting for this landblock,
|
||||
// merge them into the render-thread-owned mutable entity bucket before
|
||||
|
|
@ -668,26 +712,53 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
_loaded[landblock.LandblockId] = landblock;
|
||||
AddLoadedLandblockToFlatView(landblock);
|
||||
_tierByLandblock[landblock.LandblockId] = tier;
|
||||
if (_wbSpawnAdapter is not null)
|
||||
_wbSpawnAdapter.OnLandblockLoaded(
|
||||
_loaded[landblock.LandblockId],
|
||||
mergedRenderIds);
|
||||
return CreateSpatialPublication(
|
||||
_loaded[landblock.LandblockId],
|
||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>());
|
||||
}
|
||||
|
||||
// C.1.5b: fire DefaultScript for dat-hydrated entities (ServerGuid==0).
|
||||
// LiveEntityRuntime owns activation for live objects. This static-only
|
||||
// filter avoids replaying their defaults when a pending projection is
|
||||
// drained into a newly loaded landblock.
|
||||
if (_entityScriptActivator is not null)
|
||||
internal void ActivateLandblockPresentation(
|
||||
GpuLandblockSpatialPublication publication)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(publication);
|
||||
if (!publication.RequiresActivation)
|
||||
return;
|
||||
if (!_loaded.TryGetValue(publication.LandblockId, out LoadedLandblock? current)
|
||||
|| !ReferenceEquals(current, publication.Landblock))
|
||||
{
|
||||
var loadedEntities = _loaded[landblock.LandblockId].Entities;
|
||||
for (int i = 0; i < loadedEntities.Count; i++)
|
||||
{
|
||||
var e = loadedEntities[i];
|
||||
if (e.ServerGuid == 0)
|
||||
_entityScriptActivator.OnCreate(e);
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
$"Landblock 0x{publication.LandblockId:X8} changed before presentation activation.");
|
||||
}
|
||||
|
||||
_wbSpawnAdapter?.OnLandblockLoaded(
|
||||
publication.Landblock,
|
||||
publication.AdditionalRenderIds);
|
||||
|
||||
// C.1.5b: fire DefaultScript for dat-hydrated entities only.
|
||||
// EntityScriptActivator owns a retryable per-static acquisition ledger,
|
||||
// so replaying this activation suffix after a later owner failure does
|
||||
// not replay already-started defaults.
|
||||
if (_entityScriptActivator is not null)
|
||||
{
|
||||
for (int i = 0; i < publication.StaticEntities.Count; i++)
|
||||
_entityScriptActivator.OnCreate(publication.StaticEntities[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong> additionalRenderIds)
|
||||
{
|
||||
ulong[] renderIds = additionalRenderIds as ulong[]
|
||||
?? additionalRenderIds.ToArray();
|
||||
WorldEntity[] staticEntities = landblock.Entities
|
||||
.Where(static entity => entity.ServerGuid == 0)
|
||||
.ToArray();
|
||||
return new GpuLandblockSpatialPublication(
|
||||
landblock.LandblockId,
|
||||
landblock,
|
||||
renderIds,
|
||||
staticEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1228,11 +1299,50 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
IEnumerable<ulong>? additionalRenderIds = null)
|
||||
{
|
||||
using MutationBatch mutation = BeginMutationBatch();
|
||||
GpuLandblockSpatialPublication? publication =
|
||||
CommitEntitiesToExistingLandblockSpatialCore(
|
||||
landblockId,
|
||||
entities,
|
||||
additionalRenderIds,
|
||||
parkIfMissing: true);
|
||||
if (publication is null)
|
||||
return false;
|
||||
|
||||
ActivateLandblockPresentation(publication);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal GpuLandblockSpatialPublication CommitEntitiesToExistingLandblockSpatial(
|
||||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
IEnumerable<ulong>? additionalRenderIds)
|
||||
{
|
||||
GpuLandblockSpatialPublication? publication =
|
||||
CommitEntitiesToExistingLandblockSpatialCore(
|
||||
landblockId,
|
||||
entities,
|
||||
additionalRenderIds,
|
||||
parkIfMissing: false);
|
||||
return publication
|
||||
?? throw new InvalidOperationException(
|
||||
$"Landblock 0x{landblockId:X8} is not resident for an accepted promotion.");
|
||||
}
|
||||
|
||||
private GpuLandblockSpatialPublication? CommitEntitiesToExistingLandblockSpatialCore(
|
||||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
bool parkIfMissing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entities);
|
||||
|
||||
// A.5 T14 follow-up: canonicalize for symmetry with live projection placement.
|
||||
uint canonical = (landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
if (!_loaded.TryGetValue(canonical, out var lb))
|
||||
{
|
||||
if (!parkIfMissing)
|
||||
return null;
|
||||
|
||||
// Park as pending — same pattern as live projections for not-yet-loaded LBs.
|
||||
if (!_pendingByLandblock.TryGetValue(canonical, out var bucket))
|
||||
{
|
||||
|
|
@ -1263,7 +1373,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
}
|
||||
renderIds.UnionWith(additionalRenderIds);
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
List<WorldEntity> resident = MutableEntities(lb);
|
||||
for (int i = 0; i < entities.Count; i++)
|
||||
|
|
@ -1278,20 +1388,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
}
|
||||
_animatedIndexByLandblock.Remove(canonical);
|
||||
_tierByLandblock[canonical] = LandblockStreamTier.Near;
|
||||
if (_wbSpawnAdapter is not null)
|
||||
_wbSpawnAdapter.OnLandblockLoaded(_loaded[canonical], additionalRenderIds);
|
||||
|
||||
// C.1.5b: fire DefaultScript for each promoted dat-hydrated entity.
|
||||
// All entities arriving via this path are atlas-tier by construction
|
||||
// (the promotion path streams in dat-static scenery + EnvCell statics
|
||||
// + stabs per the method's class doc-comment).
|
||||
if (_entityScriptActivator is not null)
|
||||
{
|
||||
for (int i = 0; i < entities.Count; i++)
|
||||
_entityScriptActivator.OnCreate(entities[i]);
|
||||
}
|
||||
|
||||
return true;
|
||||
ulong[] effectiveRenderIds = additionalRenderIds?.ToArray() ?? Array.Empty<ulong>();
|
||||
WorldEntity[] staticEntities = entities
|
||||
.Where(static entity => entity.ServerGuid == 0)
|
||||
.ToArray();
|
||||
return new GpuLandblockSpatialPublication(
|
||||
canonical,
|
||||
_loaded[canonical],
|
||||
effectiveRenderIds,
|
||||
staticEntities);
|
||||
}
|
||||
|
||||
private void DrainVisibilityTransitions()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue