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

@ -789,11 +789,11 @@ internal sealed class LivePresentationCompositionPhase
foundation.Terrain.RemoveLandblock,
d.CellVisibility,
worldState,
envCellLease.Resource.CommitLandblock,
build => EnvCellMeshPreparationScheduler.Schedule(
prepareEnvCells: build => EnvCellMeshPreparationScheduler.Schedule(
build,
foundation.MeshAdapter.MeshManager!),
envCellLease.Resource.RemoveLandblock);
removeEnvCells: envCellLease.Resource.RemoveLandblock,
envCellPublisher: envCellLease.Resource);
var landblockPhysicsPublisher = new LandblockPhysicsPublisher(
d.PhysicsEngine,
world.TerrainBuild.HeightTable);

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Wb;
@ -32,6 +33,30 @@ namespace AcDream.App.Rendering.Wb;
/// <c>docs/research/named-retail/acclient.h:32035</c> (<c>BuildInfo</c>) and
/// <c>:32094</c> (<c>CBldPortal</c>).</para>
/// </summary>
internal sealed class BuildingRegistryPublication
{
internal BuildingRegistryPublication(
BuildingInfo[] buildings,
uint landblockId,
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
{
Buildings = buildings;
LandblockId = landblockId;
CellsByCellId = cellsByCellId;
PreparationCommitted = buildings.Length == 0;
}
internal BuildingInfo[] Buildings { get; }
internal uint LandblockId { get; }
internal IReadOnlyDictionary<uint, LoadedCell> CellsByCellId { get; }
internal BuildingRegistry Registry { get; } = new();
internal List<(LoadedCell Cell, uint BuildingId)> CellStamps { get; } = new();
internal int BuildingCursor { get; set; }
internal uint NextBuildingId { get; set; } = 1;
internal bool PreparationCommitted { get; set; }
internal bool PublicationCommitted { get; set; }
}
public static class BuildingLoader
{
/// <summary>
@ -39,122 +64,167 @@ public static class BuildingLoader
/// Building IDs are allocated sequentially starting at 1 (0 is reserved for
/// "no building" semantics used by <c>LoadedCell.BuildingId</c> in RR4).
/// </summary>
/// <param name="info">The dat-loaded <see cref="LandBlockInfo"/> for this landblock.</param>
/// <param name="landblockId">The 32-bit landblock id (e.g. <c>0xA9B40000</c>).
/// The high 16 bits are ORed with each 16-bit <c>OtherCellId</c> to produce
/// the full cell id.</param>
/// <param name="cellsByCellId">Pre-loaded cells keyed by full 32-bit cell id.
/// Used for BFS extension (Step B) and exit-polygon collection (Step C).
/// An empty dict is valid for unit tests; Step B and C are skipped per cell.</param>
/// <returns>A fully populated <see cref="BuildingRegistry"/>; never null.</returns>
public static BuildingRegistry Build(
LandBlockInfo info,
uint landblockId,
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
{
var reg = new BuildingRegistry();
if (info.Buildings is null || info.Buildings.Count == 0)
return reg;
uint lbMask = landblockId & 0xFFFF0000u;
uint nextId = 1;
foreach (var bInfo in info.Buildings)
BuildingRegistryPublication publication = PreparePublication(
info,
landblockId,
cellsByCellId);
while (!AdvancePreparationOne(publication))
{
var envCellIds = new HashSet<uint>();
var exitPortalPolys = new List<Vector3[]>();
}
CommitPublication(publication);
return publication.Registry;
}
// Step A: seed the cell set from BuildingInfo.Portals (entry portals).
// Each BuildingPortal.OtherCellId is a 16-bit cell-local id; OR with
// the landblock prefix for the full id.
// Defensive: skip OtherCellId == 0xFFFF (exit-portal sentinel in
// BuildingInfo — rare but WB guards against it too at PortalService.cs:58).
if (bInfo.Portals is not null)
internal static BuildingRegistryPublication PreparePublication(
LandBlockInfo info,
uint landblockId,
IReadOnlyDictionary<uint, LoadedCell> cellsByCellId)
{
ArgumentNullException.ThrowIfNull(info);
ArgumentNullException.ThrowIfNull(cellsByCellId);
int buildingCount = info.Buildings?.Count ?? 0;
var buildings = new BuildingInfo[buildingCount];
for (int index = 0; index < buildingCount; index++)
buildings[index] = info.Buildings![index];
return new BuildingRegistryPublication(
buildings,
landblockId,
cellsByCellId);
}
internal static bool AdvancePreparationOne(
BuildingRegistryPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (publication.PreparationCommitted)
return true;
if (publication.BuildingCursor >= publication.Buildings.Length)
{
publication.PreparationCommitted = true;
return true;
}
AddBuilding(
publication,
publication.Buildings[publication.BuildingCursor]);
publication.BuildingCursor++;
if (publication.BuildingCursor >= publication.Buildings.Length)
publication.PreparationCommitted = true;
return publication.PreparationCommitted;
}
internal static void CommitPublication(
BuildingRegistryPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!publication.PreparationCommitted)
{
throw new InvalidOperationException(
"A building registry cannot publish before preparation completes.");
}
if (publication.PublicationCommitted)
return;
foreach ((LoadedCell cell, uint buildingId) in publication.CellStamps)
cell.BuildingId = buildingId;
publication.PublicationCommitted = true;
}
private static void AddBuilding(
BuildingRegistryPublication publication,
BuildingInfo bInfo)
{
uint lbMask = publication.LandblockId & 0xFFFF0000u;
var envCellIds = new HashSet<uint>();
var exitPortalPolys = new List<Vector3[]>();
// Step A: seed the cell set from BuildingInfo.Portals.
if (bInfo.Portals is not null)
{
foreach (var portal in bInfo.Portals)
{
foreach (var portal in bInfo.Portals)
{
if (portal.OtherCellId == 0xFFFF) continue;
envCellIds.Add(lbMask | portal.OtherCellId);
}
}
// Step B: BFS through interior CellPortals to find the full cell set.
// Uses pre-loaded LoadedCell.Portals (avoids a duplicate dat fetch per
// BFS step). Mirrors WB PortalService.cs:67-79.
// When cellsByCellId is empty (unit-test path), BFS immediately exits.
var queue = new Queue<uint>(envCellIds);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (!cellsByCellId.TryGetValue(current, out var cell)) continue;
foreach (var p in cell.Portals)
{
if (p.OtherCellId == 0xFFFF) continue; // exit portal — stop BFS here
uint neighbourId = lbMask | p.OtherCellId;
if (envCellIds.Add(neighbourId))
queue.Enqueue(neighbourId);
}
}
// Step C: collect exit portal polygons in world space.
// For each interior cell, iterate its portals; for each exit portal
// (OtherCellId == 0xFFFF), transform the portal polygon vertices from
// cell-local space to world space via WorldTransform.
// Mirrors WB PortalService.cs:81-86 (GetPortalsForCell return path).
foreach (var cellId in envCellIds)
{
if (!cellsByCellId.TryGetValue(cellId, out var cell)) continue;
for (int pi = 0; pi < cell.Portals.Count; pi++)
{
if (cell.Portals[pi].OtherCellId != 0xFFFF) continue;
if (pi >= cell.PortalPolygons.Count) continue;
var localPoly = cell.PortalPolygons[pi];
if (localPoly.Length < 3) continue;
var worldPoly = new Vector3[localPoly.Length];
for (int v = 0; v < localPoly.Length; v++)
worldPoly[v] = Vector3.Transform(localPoly[v], cell.WorldTransform);
exitPortalPolys.Add(worldPoly);
}
}
bool hasPortalBounds = false;
var portalMin = new Vector3(float.MaxValue);
var portalMax = new Vector3(float.MinValue);
foreach (var poly in exitPortalPolys)
{
foreach (var v in poly)
{
hasPortalBounds = true;
portalMin = Vector3.Min(portalMin, v);
portalMax = Vector3.Max(portalMax, v);
}
}
// WB PortalService.cs:89: skip buildings with no interior cells.
if (envCellIds.Count == 0) continue;
var building = new Building
{
BuildingId = nextId++,
EnvCellIds = envCellIds,
ExitPortalPolygons = exitPortalPolys,
HasPortalBounds = hasPortalBounds,
PortalBounds = hasPortalBounds
? new WbBoundingBox(portalMin, portalMax)
: default,
};
reg.Add(building);
// Step 4: stamp BuildingId on each cell (Option C — both directions
// O(1)). The internal setter on LoadedCell.BuildingId is accessible
// because this class lives in the same assembly (AcDream.App).
foreach (var cellId in envCellIds)
{
if (cellsByCellId.TryGetValue(cellId, out var cell))
cell.BuildingId = building.BuildingId;
if (portal.OtherCellId == 0xFFFF) continue;
envCellIds.Add(lbMask | portal.OtherCellId);
}
}
return reg;
// Step B: BFS through interior portals.
var queue = new Queue<uint>(envCellIds);
while (queue.Count > 0)
{
uint current = queue.Dequeue();
if (!publication.CellsByCellId.TryGetValue(current, out var cell))
continue;
foreach (var portal in cell.Portals)
{
if (portal.OtherCellId == 0xFFFF) continue;
uint neighbourId = lbMask | portal.OtherCellId;
if (envCellIds.Add(neighbourId))
queue.Enqueue(neighbourId);
}
}
// Step C: collect exit portal polygons in world space.
foreach (uint cellId in envCellIds)
{
if (!publication.CellsByCellId.TryGetValue(cellId, out var cell))
continue;
for (int portalIndex = 0; portalIndex < cell.Portals.Count; portalIndex++)
{
if (cell.Portals[portalIndex].OtherCellId != 0xFFFF) continue;
if (portalIndex >= cell.PortalPolygons.Count) continue;
Vector3[] localPolygon = cell.PortalPolygons[portalIndex];
if (localPolygon.Length < 3) continue;
var worldPolygon = new Vector3[localPolygon.Length];
for (int vertexIndex = 0; vertexIndex < localPolygon.Length; vertexIndex++)
{
worldPolygon[vertexIndex] = Vector3.Transform(
localPolygon[vertexIndex],
cell.WorldTransform);
}
exitPortalPolys.Add(worldPolygon);
}
}
bool hasPortalBounds = false;
var portalMin = new Vector3(float.MaxValue);
var portalMax = new Vector3(float.MinValue);
foreach (Vector3[] polygon in exitPortalPolys)
{
foreach (Vector3 vertex in polygon)
{
hasPortalBounds = true;
portalMin = Vector3.Min(portalMin, vertex);
portalMax = Vector3.Max(portalMax, vertex);
}
}
if (envCellIds.Count == 0)
return;
uint buildingId = publication.NextBuildingId++;
publication.Registry.Add(new Building
{
BuildingId = buildingId,
EnvCellIds = envCellIds,
ExitPortalPolygons = exitPortalPolys,
HasPortalBounds = hasPortalBounds,
PortalBounds = hasPortalBounds
? new WbBoundingBox(portalMin, portalMax)
: default,
});
// Stamps remain off-side until the complete registry commits.
foreach (uint cellId in envCellIds)
{
if (publication.CellsByCellId.TryGetValue(cellId, out var cell))
publication.CellStamps.Add((cell, buildingId));
}
}
}

View file

@ -0,0 +1,45 @@
using System.Numerics;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Narrow update-thread publication seam used by streaming. Preparation builds
/// one private shell at a time; commit publishes the completed immutable
/// landblock snapshot with one owner dictionary replacement.
/// </summary>
public interface IEnvCellLandblockPublisher
{
EnvCellLandblockPublication PreparePublication(
EnvCellLandblockBuild build);
bool AdvancePreparationOne(EnvCellLandblockPublication publication);
void CommitPublication(EnvCellLandblockPublication publication);
}
public sealed class EnvCellLandblockPublication
{
internal EnvCellLandblockPublication(
object owner,
EnvCellLandblockBuild build)
{
Owner = owner;
Build = build;
Replacement = new EnvCellLandblock
{
GridX = (int)((build.LandblockId >> 24) & 0xFFu),
GridY = (int)((build.LandblockId >> 16) & 0xFFu),
};
TotalBounds = new WbBoundingBox(
new Vector3(float.MaxValue),
new Vector3(float.MinValue));
}
internal object Owner { get; }
internal EnvCellLandblockBuild Build { get; }
internal EnvCellLandblock Replacement { get; }
internal WbBoundingBox TotalBounds { get; set; }
internal int ShellCursor { get; set; }
internal bool PreparationCommitted { get; set; }
internal bool PublicationCommitted { get; set; }
}

View file

@ -29,8 +29,11 @@ using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
public sealed unsafe class EnvCellRenderer : IDisposable
public sealed unsafe class EnvCellRenderer :
IDisposable,
IEnvCellLandblockPublisher
{
private readonly object _publicationOwner = new();
private readonly GL _gl;
private readonly ObjectMeshManager _meshManager;
private readonly WbFrustum _frustum;
@ -381,8 +384,80 @@ public sealed unsafe class EnvCellRenderer : IDisposable
/// </summary>
public void CommitLandblock(EnvCellLandblockBuild build)
{
_landblocks[build.LandblockId] = CreateCommittedSnapshot(build);
EnvCellLandblockPublication publication = PreparePublication(build);
while (!AdvancePreparationOne(publication))
{
}
CommitPublication(publication);
}
EnvCellLandblockPublication IEnvCellLandblockPublisher.PreparePublication(
EnvCellLandblockBuild build) =>
PreparePublication(build);
bool IEnvCellLandblockPublisher.AdvancePreparationOne(
EnvCellLandblockPublication publication) =>
AdvancePreparationOne(publication);
void IEnvCellLandblockPublisher.CommitPublication(
EnvCellLandblockPublication publication) =>
CommitPublication(publication);
internal EnvCellLandblockPublication PreparePublication(
EnvCellLandblockBuild build)
{
ArgumentNullException.ThrowIfNull(build);
return new EnvCellLandblockPublication(_publicationOwner, build);
}
internal bool AdvancePreparationOne(
EnvCellLandblockPublication publication)
{
ValidatePublication(publication);
if (publication.PreparationCommitted)
return true;
if (publication.ShellCursor < publication.Build.Shells.Length)
{
AddShell(
publication.Replacement,
publication.Build.Shells[publication.ShellCursor]);
WbBoundingBox bounds =
publication.Build.Shells[publication.ShellCursor].WorldBounds;
publication.TotalBounds =
publication.ShellCursor == 0
? bounds
: WbBoundingBox.Union(
publication.TotalBounds,
bounds);
publication.ShellCursor++;
return false;
}
publication.Replacement.TotalEnvCellBounds =
publication.Replacement.EnvCellBounds.Count == 0
? new WbBoundingBox(Vector3.Zero, Vector3.Zero)
: publication.TotalBounds;
publication.Replacement.InstancesReady = true;
publication.Replacement.MeshDataReady = true;
publication.Replacement.GpuReady = true;
publication.PreparationCommitted = true;
return true;
}
internal void CommitPublication(
EnvCellLandblockPublication publication)
{
ValidatePublication(publication);
if (!publication.PreparationCommitted)
throw new InvalidOperationException(
"EnvCell landblock publication cannot commit before preparation.");
if (publication.PublicationCommitted)
return;
_landblocks[publication.Build.LandblockId] = publication.Replacement;
NeedsPrepare = true;
publication.PublicationCommitted = true;
}
/// <summary>
@ -398,35 +473,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
};
foreach (var shell in build.Shells)
{
replacement.Instances.Add(new EnvCellSceneryInstance
{
ObjectId = shell.GeometryId,
InstanceId = shell.CellId,
IsBuilding = true,
IsEntryCell = false,
WorldPosition = shell.WorldPosition,
LocalPosition = Vector3.Zero,
Rotation = shell.Rotation,
Scale = Vector3.One,
Transform = shell.Transform,
LocalBoundingBox = shell.LocalBounds,
BoundingBox = shell.WorldBounds,
});
replacement.EnvCellBounds[shell.CellId] = shell.WorldBounds;
if (!replacement.BuildingPartGroups.TryGetValue(shell.GeometryId, out var instances))
{
instances = new List<InstanceData>();
replacement.BuildingPartGroups[shell.GeometryId] = instances;
}
instances.Add(new InstanceData
{
Transform = shell.Transform,
CellId = shell.CellId,
Flags = 0,
});
}
AddShell(replacement, shell);
var total = new WbBoundingBox(new Vector3(float.MaxValue), new Vector3(float.MinValue));
foreach (var bounds in replacement.EnvCellBounds.Values)
@ -441,6 +488,53 @@ public sealed unsafe class EnvCellRenderer : IDisposable
return replacement;
}
private static void AddShell(
EnvCellLandblock replacement,
EnvCellShellPlacement shell)
{
replacement.Instances.Add(new EnvCellSceneryInstance
{
ObjectId = shell.GeometryId,
InstanceId = shell.CellId,
IsBuilding = true,
IsEntryCell = false,
WorldPosition = shell.WorldPosition,
LocalPosition = Vector3.Zero,
Rotation = shell.Rotation,
Scale = Vector3.One,
Transform = shell.Transform,
LocalBoundingBox = shell.LocalBounds,
BoundingBox = shell.WorldBounds,
});
replacement.EnvCellBounds[shell.CellId] = shell.WorldBounds;
if (!replacement.BuildingPartGroups.TryGetValue(
shell.GeometryId,
out var instances))
{
instances = new List<InstanceData>();
replacement.BuildingPartGroups[shell.GeometryId] = instances;
}
instances.Add(new InstanceData
{
Transform = shell.Transform,
CellId = shell.CellId,
Flags = 0,
});
}
private void ValidatePublication(
EnvCellLandblockPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!ReferenceEquals(publication.Owner, _publicationOwner))
{
throw new ArgumentException(
"The EnvCell publication receipt belongs to another renderer.",
nameof(publication));
}
}
/// <summary>
/// Removes a landblock from the renderer. Future PrepareRenderBatches will exclude it.
/// </summary>

View file

@ -2,6 +2,7 @@ using System.Diagnostics;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.Types;
namespace AcDream.App.Streaming;
@ -15,15 +16,45 @@ public sealed class LandblockPhysicsPublication
internal LandblockPhysicsPublication(
object owner,
LandblockBuild build,
Vector3 origin)
Vector3 origin,
uint currentCellId,
BuildingInfo[] buildings,
uint[] priorStaticOwnerIds)
{
Owner = owner;
Build = build;
Origin = origin;
CurrentCellId = currentCellId;
Buildings = buildings;
PriorStaticOwnerIds = priorStaticOwnerIds;
}
internal object Owner { get; }
internal LandblockBuild Build { get; }
internal uint CurrentCellId { get; }
internal BuildingInfo[] Buildings { get; }
internal uint[] PriorStaticOwnerIds { get; }
internal SortedSet<uint> GfxObjectIdSet { get; } = new();
internal uint[] GfxObjectIds { get; set; } = Array.Empty<uint>();
internal int PreparationCursor { get; set; }
internal bool PreparationCommitted { get; set; }
internal bool PriorCacheRemoved { get; set; }
internal TerrainSurface? TerrainSurface { get; set; }
internal List<CellSurface> CellSurfaces { get; } = new();
internal List<PortalPlane> PortalPlanes { get; } = new();
internal uint CellCursor { get; set; }
internal int BuildingCursor { get; set; }
internal bool BaseCommitted { get; set; }
internal int GfxCursor { get; set; }
internal int PriorStaticCursor { get; set; }
internal int StaticCursor { get; set; }
internal int BspOwnerCount { get; set; }
internal int CylinderOwnerCount { get; set; }
internal int NoCollisionCount { get; set; }
internal int SceneryTried { get; set; }
internal uint[]? RefloodOwnerIds { get; set; }
internal int RefloodCursor { get; set; }
internal bool RefloodCommitted { get; set; }
internal bool BeginCommitted { get; set; }
internal bool CompletionCommitted { get; set; }
@ -142,6 +173,21 @@ public sealed class LandblockPhysicsPublisher
/// </summary>
public LandblockPhysicsPublication PreparePublication(
LandblockRenderPublication renderPublication)
{
LandblockPhysicsPublication publication =
CreatePublication(renderPublication);
while (!AdvancePreparationOne(publication))
{
}
return publication;
}
/// <summary>
/// Captures the immutable physics root without walking the entity suffix.
/// The concrete pipeline advances that walk under its frame meter.
/// </summary>
internal LandblockPhysicsPublication CreatePublication(
LandblockRenderPublication renderPublication)
{
ArgumentNullException.ThrowIfNull(renderPublication);
LandblockBuild build = renderPublication.Build;
@ -149,10 +195,50 @@ public sealed class LandblockPhysicsPublisher
if (!IsFinite(origin))
throw new ArgumentOutOfRangeException(nameof(renderPublication));
PhysicsDatBundle datBundle =
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
?? Array.Empty<BuildingInfo>();
return new LandblockPhysicsPublication(
_receiptOwner,
build,
origin);
origin,
_physicsDataCache.CellGraph.CurrCell?.Id ?? 0u,
buildings,
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
build.Landblock.LandblockId));
}
/// <summary>
/// Advances one entity's stable GfxObj-index preparation. No external
/// physics state is mutated until this cursor and the static-validation
/// cursor both complete.
/// </summary>
internal bool AdvancePreparationOne(
LandblockPhysicsPublication publication)
{
ValidateReceipt(publication);
if (publication.PreparationCommitted)
return true;
IReadOnlyList<WorldEntity> entities =
publication.Build.Landblock.Entities;
if (publication.PreparationCursor < entities.Count)
{
WorldEntity entity = entities[publication.PreparationCursor];
for (int i = 0; i < entity.MeshRefs.Count; i++)
{
uint id = entity.MeshRefs[i].GfxObjId;
if ((id & 0xFF000000u) == 0x01000000u)
publication.GfxObjectIdSet.Add(id);
}
publication.PreparationCursor++;
return false;
}
publication.GfxObjectIds = publication.GfxObjectIdSet.ToArray();
publication.PreparationCommitted = true;
return true;
}
/// <summary>
@ -160,10 +246,26 @@ public sealed class LandblockPhysicsPublisher
/// retained receipt.
/// </summary>
public void BeginPublication(LandblockPhysicsPublication publication)
{
ValidateReceipt(publication);
if (!publication.PreparationCommitted)
throw new InvalidOperationException(
"Physics publication cannot begin before preparation commits.");
while (!AdvanceBeginOne(publication))
{
}
}
/// <summary>
/// Advances one exact physics-prefix operation: prior-cache withdrawal,
/// terrain construction, one EnvCell, one building, or the final canonical
/// physics-engine replacement. All arrays and cursors live on the receipt.
/// </summary>
internal bool AdvanceBeginOne(LandblockPhysicsPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
return true;
long started = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
@ -171,157 +273,76 @@ public sealed class LandblockPhysicsPublisher
LoadedLandblock landblock = build.Landblock;
PhysicsDatBundle datBundle =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
uint currentCellId = _physicsDataCache.CellGraph.CurrCell?.Id ?? 0u;
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
// A publication is an exact replacement for this landblock prefix.
// CacheCellStruct/CacheBuilding intentionally use retail first-wins
// semantics within one pass, so retire the prior pass before adding
// the replacement. Adjacent prefixes remain untouched.
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
landblock.LandblockId);
// TerrainInfo.Type occupies bits 2-6. The low byte retains those bits
// plus Road (bits 0-1), which TerrainSurface masks independently.
var terrainBytes = new byte[81];
for (int i = 0; i < terrainBytes.Length; i++)
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
var terrainSurface = new TerrainSurface(
landblock.Heightmap.Height,
_heightTable,
landblockX,
landblockY,
terrainBytes);
var cellSurfaces = new List<CellSurface>();
var portalPlanes = new List<PortalPlane>();
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo = datBundle.Info;
if (landblockInfo is not null && landblockInfo.NumCells > 0)
if (!publication.PriorCacheRemoved)
{
uint firstCellId =
(landblock.LandblockId & 0xFFFF0000u) | 0x0100u;
for (uint offset = 0; offset < landblockInfo.NumCells; offset++)
// CacheCellStruct/CacheBuilding use first-wins semantics within one
// publication, so the replacement pass starts with one exact
// landblock-scoped withdrawal.
_physicsDataCache.RemoveCellsForLandblock(landblock.LandblockId);
_physicsDataCache.RemoveBuildingsForLandblock(landblock.LandblockId);
_physicsDataCache.CellGraph.RemoveEnvCellsForLandblock(
landblock.LandblockId);
publication.PriorCacheRemoved = true;
}
else if (publication.TerrainSurface is null)
{
uint landblockX = (landblock.LandblockId >> 24) & 0xFFu;
uint landblockY = (landblock.LandblockId >> 16) & 0xFFu;
var terrainBytes = new byte[81];
for (int i = 0; i < terrainBytes.Length; i++)
terrainBytes[i] = (byte)(ushort)landblock.Heightmap.Terrain[i];
publication.TerrainSurface = new TerrainSurface(
landblock.Heightmap.Height,
_heightTable,
landblockX,
landblockY,
terrainBytes);
}
else if (landblockInfo is not null
&& publication.CellCursor < landblockInfo.NumCells)
{
PublishCell(publication, datBundle, publication.CellCursor);
publication.CellCursor++;
}
else if (publication.BuildingCursor < publication.Buildings.Length)
{
PublishBuilding(
landblock,
datBundle,
publication.TerrainSurface,
origin,
publication.Buildings[publication.BuildingCursor]);
publication.BuildingCursor++;
}
else if (!publication.BaseCommitted)
{
_physicsEngine.AddLandblock(
landblock.LandblockId,
publication.TerrainSurface,
publication.CellSurfaces,
publication.PortalPlanes,
origin.X,
origin.Y);
if ((publication.CurrentCellId & 0xFFFF0000u)
== (landblock.LandblockId & 0xFFFF0000u))
{
uint envCellId = firstCellId + offset;
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell))
continue;
if (envCell.EnvironmentId == 0)
continue;
if (!datBundle.Environments.TryGetValue(
0x0D000000u | envCell.EnvironmentId,
out var environment))
{
continue;
}
if (!environment.Cells.TryGetValue(
envCell.CellStructure,
out var cellStruct))
{
continue;
}
Quaternion rotation = envCell.Position.Orientation;
Vector3 cellOriginWorld = envCell.Position.Origin + origin;
Matrix4x4 physicsCellTransform =
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(cellOriginWorld);
_physicsDataCache.CacheCellStruct(
envCellId,
envCell,
cellStruct,
physicsCellTransform);
var worldVertices = new Dictionary<ushort, Vector3>(
cellStruct.VertexArray.Vertices.Count);
foreach ((ushort vertexId, var vertex) in
cellStruct.VertexArray.Vertices)
{
Vector3 worldPosition =
Vector3.Transform(vertex.Origin, rotation)
+ cellOriginWorld;
worldVertices[(ushort)vertexId] = worldPosition;
}
var polygonVertexIds = new List<List<short>>(
cellStruct.PhysicsPolygons.Count);
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
{
var vertexIds = new List<short>(polygon.VertexIds.Count);
foreach (short vertexId in polygon.VertexIds)
vertexIds.Add(vertexId);
polygonVertexIds.Add(vertexIds);
}
cellSurfaces.Add(new CellSurface(
envCellId,
worldVertices,
polygonVertexIds));
// CellPortal.PolygonId indexes rendering Polygons, not
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
foreach (var portal in envCell.CellPortals)
{
if (!cellStruct.Polygons.TryGetValue(
portal.PolygonId,
out var polygon)
|| polygon.VertexIds.Count < 3)
{
continue;
}
var portalVertices = new Vector3[polygon.VertexIds.Count];
bool allFound = true;
for (int index = 0; index < polygon.VertexIds.Count; index++)
{
if (!worldVertices.TryGetValue(
(ushort)polygon.VertexIds[index],
out portalVertices[index]))
{
allFound = false;
break;
}
}
if (!allFound)
continue;
portalPlanes.Add(PortalPlane.FromVertices(
portalVertices.AsSpan(),
portal.OtherCellId,
envCellId & 0xFFFFu,
(ushort)portal.Flags));
}
_physicsEngine.UpdatePlayerCurrCell(publication.CurrentCellId);
}
publication.BaseCommitted = true;
}
int buildingCount = PublishBuildings(
landblock,
datBundle,
landblockInfo,
terrainSurface,
origin);
_physicsEngine.AddLandblock(
landblock.LandblockId,
terrainSurface,
cellSurfaces,
portalPlanes,
origin.X,
origin.Y);
if ((currentCellId & 0xFFFF0000u)
== (landblock.LandblockId & 0xFFFF0000u))
else
{
_physicsEngine.UpdatePlayerCurrCell(currentCellId);
_cellSurfaceCount += publication.CellSurfaces.Count;
_portalPlaneCount += publication.PortalPlanes.Count;
_buildingCount += publication.Buildings.Length;
publication.BeginCommitted = true;
_beginCount++;
}
_cellSurfaceCount += cellSurfaces.Count;
_portalPlaneCount += portalPlanes.Count;
_buildingCount += buildingCount;
publication.BeginCommitted = true;
_beginCount++;
_basePublishTicks += Stopwatch.GetTimestamp() - started;
return publication.BeginCommitted;
}
/// <summary>
@ -338,209 +359,93 @@ public sealed class LandblockPhysicsPublisher
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return;
while (!AdvanceCompleteOne(publication, beforeStaticCollision))
{
}
}
long completedStarted = Stopwatch.GetTimestamp();
/// <summary>
/// Advances one exact physics-suffix operation: one GfxObj cache entry,
/// prior static withdrawal, one static light/collision entity, or reflood.
/// </summary>
internal bool AdvanceCompleteOne(
LandblockPhysicsPublication publication,
Action<WorldEntity>? beforeStaticCollision = null)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Physics publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return true;
long started = Stopwatch.GetTimestamp();
LoadedLandblock landblock = publication.Build.Landblock;
PhysicsDatBundle datBundle =
landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
long cacheStarted = Stopwatch.GetTimestamp();
foreach (WorldEntity entity in landblock.Entities)
if (publication.GfxCursor < publication.GfxObjectIds.Length)
{
foreach (MeshRef meshRef in entity.MeshRefs)
{
if ((meshRef.GfxObjId & 0xFF000000u) != 0x01000000u)
continue;
if (datBundle.GfxObjs.TryGetValue(meshRef.GfxObjId, out var gfx))
_physicsDataCache.CacheGfxObj(meshRef.GfxObjId, gfx);
}
long cacheStarted = Stopwatch.GetTimestamp();
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
publication.GfxCursor++;
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
}
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
// The immutable entity list is an exact Near snapshot. Retire every
// static owner created by the prior snapshot before registering this
// one; RegisterMultiPart can replace matching IDs but cannot discover
// owners omitted by a ForceReload/reapply. Logical-owner removal also
// clears footprints flooded across an adjacent landblock seam.
_physicsEngine.ShadowObjects.DeregisterStaticOwnersForLandblock(
landblock.LandblockId);
int landblockBspCount = 0;
int landblockCylinderCount = 0;
int landblockNoCollisionCount = 0;
int sceneryTried = 0;
foreach (WorldEntity entity in landblock.Entities)
else if (publication.PriorStaticCursor
< publication.PriorStaticOwnerIds.Length)
{
_physicsEngine.ShadowObjects.DeregisterStaticOwnerForLandblock(
publication.PriorStaticOwnerIds[
publication.PriorStaticCursor],
landblock.LandblockId);
publication.PriorStaticCursor++;
}
else if (publication.StaticCursor < landblock.Entities.Count)
{
WorldEntity entity = landblock.Entities[publication.StaticCursor];
beforeStaticCollision?.Invoke(entity);
// Retail building shells collide exclusively through the
// per-landcell building channel. They never become ordinary
// shadow objects, even when their source Setup has cylinders.
if (entity.IsBuildingShell)
continue;
int entityBspCount = 0;
int entityCylinderCount = 0;
uint sourcePrefix =
entity.SourceGfxObjOrSetupId & 0xFF000000u;
bool isOutdoorMesh =
(entity.Id & 0x80000000u) != 0
|| (entity.Id < 0x40000000u
&& (sourcePrefix == 0x01000000u
|| sourcePrefix == 0x02000000u));
if (isOutdoorMesh)
sceneryTried++;
IReadOnlyList<ShadowShape> bspShapes =
ShadowShapeBuilder.FromLandblockBspParts(
entity.MeshRefs,
entity.IsBuildingShell,
_physicsDataCache.GetGfxObj);
entityBspCount = bspShapes.Count;
if (entityBspCount > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
bspShapes,
0u,
EntityCollisionFlags.None,
publication.Origin.X,
publication.Origin.Y,
landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
LogMultipartRegistration(landblock, entity, bspShapes);
}
SetupPhysics? setup =
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
if (setup is not null && entityBspCount == 0)
{
float scale = entity.Scale > 0f ? entity.Scale : 1f;
var setupShapes = new List<ShadowShape>();
for (int cylinderIndex = 0;
cylinderIndex < setup.CylSpheres.Count;
cylinderIndex++)
{
DatReaderWriter.Types.CylSphere cylinder =
setup.CylSpheres[cylinderIndex];
float radius = cylinder.Radius * scale;
float baseHeight = cylinder.Height > 0f
? cylinder.Height
: cylinder.Radius * 4f;
float height = baseHeight * scale;
if (radius <= 0f)
continue;
Vector3 localOffset = cylinder.Origin * scale;
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: localOffset,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: height));
}
if (setup.CylSpheres.Count == 0)
{
for (int sphereIndex = 0;
sphereIndex < setup.Spheres.Count;
sphereIndex++)
{
DatReaderWriter.Types.Sphere sphere =
setup.Spheres[sphereIndex];
if (sphere.Radius <= 0f)
continue;
float radius = sphere.Radius * scale;
Vector3 localOffset = sphere.Origin * scale;
Vector3 localBaseOffset = localOffset
+ Vector3.Transform(
-Vector3.UnitZ * radius,
Quaternion.Inverse(entity.Rotation));
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: localBaseOffset,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: radius * 2f));
}
}
if (setup.CylSpheres.Count == 0
&& setup.Spheres.Count == 0
&& setup.Radius > 0f)
{
float radius = setup.Radius * scale;
float height = (setup.Height > 0f
? setup.Height
: setup.Radius * 2f) * scale;
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: Vector3.Zero,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: height));
}
if (setupShapes.Count > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
setupShapes,
0u,
EntityCollisionFlags.None,
publication.Origin.X,
publication.Origin.Y,
landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
LogSetupRegistration(landblock, entity, setupShapes);
entityCylinderCount = setupShapes.Count;
}
}
if (entityBspCount > 0)
landblockBspCount++;
if (entityCylinderCount > 0)
landblockCylinderCount++;
if (entityBspCount == 0 && entityCylinderCount == 0
&& (sourcePrefix == 0x01000000u
|| sourcePrefix == 0x02000000u))
{
landblockNoCollisionCount++;
}
PublishStaticEntity(publication, entity);
publication.StaticCursor++;
}
if (PhysicsDiagnostics.ProbeBuildingEnabled && sceneryTried > 0)
else if (publication.RefloodOwnerIds is null)
{
Console.WriteLine(
$"lb 0x{landblock.LandblockId:X8}: scenery tried={sceneryTried} " +
$"(outdoorNone={landblockNoCollisionCount})");
publication.RefloodOwnerIds =
_physicsEngine.ShadowObjects
.CaptureRefloodOwnersForLandblock(landblock.LandblockId);
}
else if (publication.RefloodCursor
< publication.RefloodOwnerIds.Length)
{
_physicsEngine.ShadowObjects.RefloodOwnerForLandblock(
publication.RefloodOwnerIds[publication.RefloodCursor],
landblock.LandblockId);
publication.RefloodCursor++;
}
else if (!publication.RefloodCommitted)
{
if (PhysicsDiagnostics.ProbeBuildingEnabled
&& publication.SceneryTried > 0)
{
Console.WriteLine(
$"lb 0x{landblock.LandblockId:X8}: scenery tried={publication.SceneryTried} " +
$"(outdoorNone={publication.NoCollisionCount})");
}
LogMissingSceneryBounds(landblock);
_refloodCount++;
publication.RefloodCommitted = true;
}
else
{
_staticBspOwnerCount += publication.BspOwnerCount;
_staticCylinderOwnerCount += publication.CylinderOwnerCount;
publication.CompletionCommitted = true;
_completeCount++;
}
LogMissingSceneryBounds(landblock);
_physicsEngine.ShadowObjects.RefloodLandblock(landblock.LandblockId);
_refloodCount++;
_staticBspOwnerCount += landblockBspCount;
_staticCylinderOwnerCount += landblockCylinderCount;
publication.CompletionCommitted = true;
_completeCount++;
_completePublishTicks +=
Stopwatch.GetTimestamp() - completedStarted;
_completePublishTicks += Stopwatch.GetTimestamp() - started;
return publication.CompletionCommitted;
}
public void DemoteToTerrain(uint landblockId)
@ -555,54 +460,292 @@ public sealed class LandblockPhysicsPublisher
_fullRemovalCount++;
}
private int PublishBuildings(
private void PublishCell(
LandblockPhysicsPublication publication,
PhysicsDatBundle datBundle,
uint offset)
{
LoadedLandblock landblock = publication.Build.Landblock;
uint envCellId =
(landblock.LandblockId & 0xFFFF0000u) | (0x0100u + offset);
if (!datBundle.EnvCells.TryGetValue(envCellId, out var envCell)
|| envCell.EnvironmentId == 0
|| !datBundle.Environments.TryGetValue(
0x0D000000u | envCell.EnvironmentId,
out var environment)
|| !environment.Cells.TryGetValue(
envCell.CellStructure,
out var cellStruct))
{
return;
}
Quaternion rotation = envCell.Position.Orientation;
Vector3 cellOriginWorld = envCell.Position.Origin + publication.Origin;
Matrix4x4 physicsCellTransform =
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(cellOriginWorld);
_physicsDataCache.CacheCellStruct(
envCellId,
envCell,
cellStruct,
physicsCellTransform);
var worldVertices = new Dictionary<ushort, Vector3>(
cellStruct.VertexArray.Vertices.Count);
foreach ((ushort vertexId, var vertex) in
cellStruct.VertexArray.Vertices)
{
Vector3 worldPosition =
Vector3.Transform(vertex.Origin, rotation)
+ cellOriginWorld;
worldVertices[(ushort)vertexId] = worldPosition;
}
var polygonVertexIds = new List<List<short>>(
cellStruct.PhysicsPolygons.Count);
foreach (var polygon in cellStruct.PhysicsPolygons.Values)
{
var vertexIds = new List<short>(polygon.VertexIds.Count);
foreach (short vertexId in polygon.VertexIds)
vertexIds.Add(vertexId);
polygonVertexIds.Add(vertexIds);
}
var portalPlanes = new List<PortalPlane>();
// CellPortal.PolygonId indexes rendering Polygons, not
// PhysicsPolygons (ACViewer EnvCell.find_transit_cells).
foreach (var portal in envCell.CellPortals)
{
if (!cellStruct.Polygons.TryGetValue(
portal.PolygonId,
out var polygon)
|| polygon.VertexIds.Count < 3)
{
continue;
}
var portalVertices = new Vector3[polygon.VertexIds.Count];
bool allFound = true;
for (int index = 0; index < polygon.VertexIds.Count; index++)
{
if (!worldVertices.TryGetValue(
(ushort)polygon.VertexIds[index],
out portalVertices[index]))
{
allFound = false;
break;
}
}
if (!allFound)
continue;
portalPlanes.Add(PortalPlane.FromVertices(
portalVertices.AsSpan(),
portal.OtherCellId,
envCellId & 0xFFFFu,
(ushort)portal.Flags));
}
publication.CellSurfaces.Add(new CellSurface(
envCellId,
worldVertices,
polygonVertexIds));
publication.PortalPlanes.AddRange(portalPlanes);
}
private void PublishBuilding(
LoadedLandblock landblock,
PhysicsDatBundle datBundle,
DatReaderWriter.DBObjs.LandBlockInfo? landblockInfo,
TerrainSurface terrainSurface,
Vector3 origin)
Vector3 origin,
BuildingInfo building)
{
if (landblockInfo is null || landblockInfo.Buildings.Count == 0)
return 0;
uint landblockPrefix = landblock.LandblockId & 0xFFFF0000u;
foreach (var building in landblockInfo.Buildings)
var portals = new List<BldPortalInfo>(building.Portals.Count);
foreach (var portal in building.Portals)
{
var portals = new List<BldPortalInfo>(building.Portals.Count);
foreach (var portal in building.Portals)
{
portals.Add(new BldPortalInfo(
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
otherPortalId: unchecked((short)portal.OtherPortalId),
flags: (ushort)portal.Flags));
}
Vector3 buildingOrigin = building.Frame.Origin + origin;
Matrix4x4 buildingTransform =
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
* Matrix4x4.CreateTranslation(buildingOrigin);
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
building.Frame.Origin.X,
building.Frame.Origin.Y);
uint landcellId = landblockPrefix | landcellLow;
uint shellPartZero = building.ModelId;
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
{
datBundle.Setups.TryGetValue(
building.ModelId,
out var setup);
shellPartZero = setup is not null && setup.Parts.Count > 0
? setup.Parts[0]
: 0u;
}
_physicsDataCache.CacheBuilding(
landcellId,
portals,
buildingTransform,
modelId: shellPartZero);
portals.Add(new BldPortalInfo(
otherCellId: landblockPrefix | (uint)portal.OtherCellId,
otherPortalId: unchecked((short)portal.OtherPortalId),
flags: (ushort)portal.Flags));
}
Vector3 buildingOrigin = building.Frame.Origin + origin;
Matrix4x4 buildingTransform =
Matrix4x4.CreateFromQuaternion(building.Frame.Orientation)
* Matrix4x4.CreateTranslation(buildingOrigin);
uint landcellLow = terrainSurface.ComputeOutdoorCellId(
building.Frame.Origin.X,
building.Frame.Origin.Y);
uint landcellId = landblockPrefix | landcellLow;
uint shellPartZero = building.ModelId;
if ((shellPartZero & 0xFF000000u) == 0x02000000u)
{
datBundle.Setups.TryGetValue(
building.ModelId,
out var setup);
shellPartZero = setup is not null && setup.Parts.Count > 0
? setup.Parts[0]
: 0u;
}
_physicsDataCache.CacheBuilding(
landcellId,
portals,
buildingTransform,
modelId: shellPartZero);
}
private void PublishStaticEntity(
LandblockPhysicsPublication publication,
WorldEntity entity)
{
LoadedLandblock landblock = publication.Build.Landblock;
// Retail building shells collide exclusively through the per-landcell
// building channel. They never become ordinary shadow objects.
if (entity.IsBuildingShell)
return;
int entityBspCount = 0;
int entityCylinderCount = 0;
uint sourcePrefix = entity.SourceGfxObjOrSetupId & 0xFF000000u;
bool isOutdoorMesh =
(entity.Id & 0x80000000u) != 0
|| (entity.Id < 0x40000000u
&& (sourcePrefix == 0x01000000u
|| sourcePrefix == 0x02000000u));
if (isOutdoorMesh)
publication.SceneryTried++;
IReadOnlyList<ShadowShape> bspShapes =
ShadowShapeBuilder.FromLandblockBspParts(
entity.MeshRefs,
entity.IsBuildingShell,
_physicsDataCache.GetGfxObj);
entityBspCount = bspShapes.Count;
if (entityBspCount > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
bspShapes,
0u,
EntityCollisionFlags.None,
publication.Origin.X,
publication.Origin.Y,
landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
LogMultipartRegistration(landblock, entity, bspShapes);
}
SetupPhysics? setup =
_physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId);
if (setup is not null && entityBspCount == 0)
{
float scale = entity.Scale > 0f ? entity.Scale : 1f;
var setupShapes = new List<ShadowShape>();
for (int cylinderIndex = 0;
cylinderIndex < setup.CylSpheres.Count;
cylinderIndex++)
{
CylSphere cylinder = setup.CylSpheres[cylinderIndex];
float radius = cylinder.Radius * scale;
float baseHeight = cylinder.Height > 0f
? cylinder.Height
: cylinder.Radius * 4f;
float height = baseHeight * scale;
if (radius <= 0f)
continue;
Vector3 localOffset = cylinder.Origin * scale;
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: localOffset,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: height));
}
if (setup.CylSpheres.Count == 0)
{
for (int sphereIndex = 0;
sphereIndex < setup.Spheres.Count;
sphereIndex++)
{
Sphere sphere = setup.Spheres[sphereIndex];
if (sphere.Radius <= 0f)
continue;
float radius = sphere.Radius * scale;
Vector3 localOffset = sphere.Origin * scale;
Vector3 localBaseOffset = localOffset
+ Vector3.Transform(
-Vector3.UnitZ * radius,
Quaternion.Inverse(entity.Rotation));
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: localBaseOffset,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: radius * 2f));
}
}
if (setup.CylSpheres.Count == 0
&& setup.Spheres.Count == 0
&& setup.Radius > 0f)
{
float radius = setup.Radius * scale;
float height = (setup.Height > 0f
? setup.Height
: setup.Radius * 2f) * scale;
setupShapes.Add(new ShadowShape(
GfxObjId: entity.SourceGfxObjOrSetupId,
LocalPosition: Vector3.Zero,
LocalRotation: Quaternion.Identity,
Scale: scale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: radius,
CylHeight: height));
}
if (setupShapes.Count > 0)
{
_physicsEngine.ShadowObjects.RegisterMultiPart(
entity.Id,
entity.Position,
entity.Rotation,
setupShapes,
0u,
EntityCollisionFlags.None,
publication.Origin.X,
publication.Origin.Y,
landblock.LandblockId,
seedCellId: entity.ParentCellId ?? 0u,
isStatic: true);
LogSetupRegistration(landblock, entity, setupShapes);
entityCylinderCount = setupShapes.Count;
}
}
if (entityBspCount > 0)
publication.BspOwnerCount++;
if (entityCylinderCount > 0)
publication.CylinderOwnerCount++;
if (entityBspCount == 0 && entityCylinderCount == 0
&& (sourcePrefix == 0x01000000u
|| sourcePrefix == 0x02000000u))
{
publication.NoCollisionCount++;
}
return landblockInfo.Buildings.Count;
}
private static void LogSetupRegistration(

View file

@ -14,6 +14,10 @@ public readonly record struct LandblockPresentationDiagnostics(
LandblockPhysicsPublisherDiagnostics Physics,
LandblockStaticPresentationDiagnostics Statics);
internal readonly record struct LandblockPublicationAdvance(
bool Completed,
bool Progressed);
/// <summary>
/// Render-thread transaction coordinator for one accepted landblock streaming
/// result. Streaming policy (desired tier, generation, priority, and apply
@ -48,6 +52,7 @@ public sealed class LandblockPresentationPipeline
public required LandblockBuild Build;
public required LandblockMeshData MeshData;
public required uint LandblockId;
public required LandblockStreamCostEstimate Cost;
public LandblockStreamTier Tier;
public bool PresentationCommitted;
public LandblockRenderPublication? RenderPublication;
@ -186,7 +191,19 @@ public sealed class LandblockPresentationPipeline
ArgumentNullException.ThrowIfNull(result);
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
return;
Advance(result, transaction);
Advance(result, transaction, meter: null, ensureProgress: false);
}
internal LandblockPublicationAdvance ResumePublication(
LandblockStreamResult result,
StreamingWorkMeter meter,
bool ensureProgress)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(meter);
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
return new LandblockPublicationAdvance(Completed: true, Progressed: false);
return Advance(result, transaction, meter, ensureProgress);
}
public void AdvanceRetirements() => _retirements.Advance();
@ -223,8 +240,28 @@ public sealed class LandblockPresentationPipeline
loaded.Build,
loaded.MeshData,
loaded.LandblockId,
loaded.Tier);
Advance(loaded, transaction);
loaded.Tier,
estimate: null);
Advance(loaded, transaction, meter: null, ensureProgress: false);
}
internal LandblockPublicationAdvance PublishLoaded(
LandblockStreamResult.Loaded loaded,
LandblockStreamCostEstimate estimate,
StreamingWorkMeter meter,
bool ensureProgress)
{
ArgumentNullException.ThrowIfNull(loaded);
ArgumentNullException.ThrowIfNull(meter);
PublicationTransaction transaction = GetOrCreate(
loaded,
PublicationKind.Loaded,
loaded.Build,
loaded.MeshData,
loaded.LandblockId,
loaded.Tier,
estimate);
return Advance(loaded, transaction, meter, ensureProgress);
}
public void PublishPromoted(
@ -241,8 +278,32 @@ public sealed class LandblockPresentationPipeline
promoted.Build,
promoted.MeshData,
promoted.LandblockId,
LandblockStreamTier.Near);
Advance(promoted, transaction);
LandblockStreamTier.Near,
estimate: null);
Advance(promoted, transaction, meter: null, ensureProgress: false);
}
internal LandblockPublicationAdvance PublishPromoted(
LandblockStreamResult.Promoted promoted,
bool mergeIntoExistingLandblock,
LandblockStreamCostEstimate estimate,
StreamingWorkMeter meter,
bool ensureProgress)
{
ArgumentNullException.ThrowIfNull(promoted);
ArgumentNullException.ThrowIfNull(meter);
PublicationKind kind = mergeIntoExistingLandblock
? PublicationKind.PromoteExisting
: PublicationKind.PromoteSelfContained;
PublicationTransaction transaction = GetOrCreate(
promoted,
kind,
promoted.Build,
promoted.MeshData,
promoted.LandblockId,
LandblockStreamTier.Near,
estimate);
return Advance(promoted, transaction, meter, ensureProgress);
}
public void PublishAsFar(
@ -270,6 +331,11 @@ public sealed class LandblockPresentationPipeline
Origin: completedBuild.Origin),
MeshData = meshData,
LandblockId = farLandblock.LandblockId,
Cost = LandblockStreamResultCost.Estimate(
new LandblockBuild(
farLandblock,
Origin: completedBuild.Origin),
meshData),
Tier = LandblockStreamTier.Far,
};
_publications.Add(acceptedResult, transaction);
@ -280,7 +346,56 @@ public sealed class LandblockPresentationPipeline
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
}
Advance(acceptedResult, transaction);
Advance(
acceptedResult,
transaction,
meter: null,
ensureProgress: false);
}
internal LandblockPublicationAdvance PublishAsFar(
LandblockStreamResult acceptedResult,
LandblockBuild completedBuild,
LandblockMeshData meshData,
StreamingWorkMeter meter,
bool ensureProgress)
{
ArgumentNullException.ThrowIfNull(acceptedResult);
ArgumentNullException.ThrowIfNull(completedBuild);
ArgumentNullException.ThrowIfNull(meshData);
ArgumentNullException.ThrowIfNull(meter);
if (!_publications.TryGetValue(
acceptedResult,
out PublicationTransaction? transaction))
{
LoadedLandblock completed = completedBuild.Landblock;
var farLandblock = new LoadedLandblock(
completed.LandblockId,
completed.Heightmap,
Array.Empty<WorldEntity>(),
PhysicsDatBundle.Empty);
LandblockBuild farBuild = new(
farLandblock,
Origin: completedBuild.Origin);
transaction = new PublicationTransaction
{
Kind = PublicationKind.Far,
Build = farBuild,
MeshData = meshData,
LandblockId = farLandblock.LandblockId,
Cost = LandblockStreamResultCost.Estimate(farBuild, meshData),
Tier = LandblockStreamTier.Far,
};
_publications.Add(acceptedResult, transaction);
}
else if (transaction.Kind != PublicationKind.Far)
{
throw new InvalidOperationException(
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
}
return Advance(acceptedResult, transaction, meter, ensureProgress);
}
private PublicationTransaction GetOrCreate(
@ -289,7 +404,8 @@ public sealed class LandblockPresentationPipeline
LandblockBuild build,
LandblockMeshData meshData,
uint landblockId,
LandblockStreamTier tier)
LandblockStreamTier tier,
LandblockStreamCostEstimate? estimate)
{
if (_publications.TryGetValue(result, out PublicationTransaction? existing))
{
@ -311,71 +427,317 @@ public sealed class LandblockPresentationPipeline
Build = build,
MeshData = meshData,
LandblockId = landblockId,
Cost = estimate
?? LandblockStreamResultCost.Estimate(build, meshData),
Tier = tier,
};
_publications.Add(result, created);
return created;
}
private void Advance(
private LandblockPublicationAdvance Advance(
LandblockStreamResult result,
PublicationTransaction transaction)
PublicationTransaction transaction,
StreamingWorkMeter? meter,
bool ensureProgress)
{
bool progressed = false;
bool TryRun(
StreamingWorkCost cost,
string stage,
Action operation)
{
if (meter is null)
{
operation();
progressed = true;
return true;
}
StreamingWorkAdmission admission = meter.TryReserve(
cost,
stage,
ensureProgress && !progressed);
if (admission == StreamingWorkAdmission.Yielded)
return false;
try
{
operation();
meter.Complete();
progressed = true;
return true;
}
catch
{
meter.Fail();
throw;
}
}
if (!transaction.PresentationCommitted)
{
if (_renderPublisher is not null
&& _physicsPublisher is not null
&& _staticPublisher is not null)
{
transaction.RenderPublication ??=
_renderPublisher.PreparePublication(
transaction.Build,
transaction.MeshData);
transaction.PhysicsPublication ??=
_physicsPublisher.PreparePublication(
transaction.RenderPublication);
transaction.StaticPublication ??=
_staticPublisher.PreparePublication(
transaction.PhysicsPublication);
while (transaction.RenderPublication is null)
{
if (!TryRun(
new StreamingWorkCost(EntityOperations: 1),
"publication-prepare-render",
() => transaction.RenderPublication =
_renderPublisher.PreparePublication(
transaction.Build,
transaction.MeshData)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (transaction.PhysicsPublication is null)
{
if (!TryRun(
default,
"publication-prepare-physics",
() => transaction.PhysicsPublication =
_physicsPublisher.CreatePublication(
transaction.RenderPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (!transaction.PhysicsPublication.PreparationCommitted)
{
int entityOperations =
transaction.PhysicsPublication.PreparationCursor
< transaction.Build.Landblock.Entities.Count
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-index-physics",
() => _physicsPublisher.AdvancePreparationOne(
transaction.PhysicsPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (transaction.StaticPublication is null)
{
if (!TryRun(
default,
"publication-prepare-statics",
() => transaction.StaticPublication =
_staticPublisher.CreatePublication(
transaction.PhysicsPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (!transaction.StaticPublication.PreparationCommitted)
{
int entityOperations =
transaction.StaticPublication.PreparationCursor
< transaction.Build.Landblock.Entities.Count
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-validate-statics",
() => _staticPublisher.AdvancePreparationOne(
transaction.StaticPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
// Prepare every immutable receipt before the first external
// mutation. Cross-owner identity/data errors therefore block
// the publication without leaving a committed prefix.
_renderPublisher.BeginPublication(transaction.RenderPublication);
_physicsPublisher.BeginPublication(transaction.PhysicsPublication);
while (!transaction.RenderPublication.BeginCommitted)
{
StreamingWorkCost cost =
!transaction.RenderPublication.TerrainCommitted
? new StreamingWorkCost(
GpuUploadBytes:
transaction.Cost.TerrainPayloadBytes)
: !transaction.RenderPublication.VisibilityCommitted
? new StreamingWorkCost(
EntityOperations:
transaction.Cost.VisibilityCells)
: default;
if (!TryRun(
cost,
"publication-render-prefix",
() => _renderPublisher.AdvanceBeginOne(
transaction.RenderPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (!transaction.PhysicsPublication.BeginCommitted)
{
int entityOperations =
transaction.PhysicsPublication.TerrainSurface is not null
&& transaction.PhysicsPublication.Build.Landblock.PhysicsDats?
.Info is { } info
&& transaction.PhysicsPublication.CellCursor < info.NumCells
? 1
: transaction.PhysicsPublication.BuildingCursor
< transaction.PhysicsPublication.Buildings.Length
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-physics-prefix",
() => _physicsPublisher.AdvanceBeginOne(
transaction.PhysicsPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
// Retail establishes the building/visible-cell suffix before
// each static object publishes lights, collision, and scripts.
_renderPublisher.CompletePublication(
transaction.RenderPublication);
while (!transaction.RenderPublication.CompletionCommitted)
{
int entityOperations =
transaction.RenderPublication.BuildingPublication is
{ PreparationCommitted: false } buildingPublication
&& buildingPublication.BuildingCursor
< buildingPublication.Buildings.Length
? 1
: transaction.RenderPublication.EnvCellPublication is
{ PreparationCommitted: false } envCellPublication
&& envCellPublication.ShellCursor
< envCellPublication.Build.Shells.Length
? 1
: !transaction.RenderPublication.EnvCellsCommitted
&& transaction.RenderPublication
.EnvCellPublication is null
? transaction.Cost.EnvCellShells
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-render-suffix",
() => _renderPublisher.AdvanceCompleteOne(
transaction.RenderPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
_staticPublisher.BeginPublication(transaction.StaticPublication);
_physicsPublisher.CompletePublication(
transaction.PhysicsPublication,
entity => _staticPublisher.PrepareEntityBeforeCollision(
transaction.StaticPublication,
entity));
_staticPublisher.CompletePublication(
transaction.StaticPublication);
while (!transaction.StaticPublication.BeginCommitted)
{
int entityOperations =
transaction.StaticPublication.PriorCleanupCursor
< transaction.StaticPublication
.OrderedPreviouslyActiveIds.Count
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-static-cleanup",
() => _staticPublisher.AdvanceBeginOne(
transaction.StaticPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (!transaction.PhysicsPublication.CompletionCommitted)
{
int entityOperations =
transaction.PhysicsPublication.GfxCursor
< transaction.PhysicsPublication.GfxObjectIds.Length
? 1
: transaction.PhysicsPublication.PriorStaticCursor
< transaction.PhysicsPublication
.PriorStaticOwnerIds.Length
? 1
: transaction.PhysicsPublication.StaticCursor
< transaction.Build.Landblock.Entities.Count
? 1
: transaction.PhysicsPublication
.RefloodOwnerIds is not null
&& transaction.PhysicsPublication
.RefloodCursor
< transaction.PhysicsPublication
.RefloodOwnerIds.Length
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-physics-statics",
() => _physicsPublisher.AdvanceCompleteOne(
transaction.PhysicsPublication,
entity =>
_staticPublisher
.PrepareEntityBeforeCollision(
transaction.StaticPublication,
entity))))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
while (!transaction.StaticPublication.CompletionCommitted)
{
int entityOperations =
transaction.StaticPublication.PluginCursor
< transaction.StaticPublication.OrderedSnapshots.Count
? 1
: 0;
if (!TryRun(
new StreamingWorkCost(
EntityOperations: entityOperations),
"publication-plugin-projection",
() => _staticPublisher.AdvanceCompleteOne(
transaction.StaticPublication)))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
}
else
{
try
StreamingWorkCost legacyCost = new(
EntityOperations: transaction.Cost.Entities,
GpuUploadBytes: transaction.Cost.TerrainPayloadBytes);
bool ran = TryRun(
legacyCost,
"publication-legacy-presentation",
() =>
{
try
{
(_publishBeforeSpatialCommit
?? throw new InvalidOperationException(
"The presentation pipeline has no publication owners."))(
transaction.Build,
transaction.MeshData);
}
catch (Exception error)
{
// Compatibility callbacks cannot report an exact retained
// receipt. Preserve the established fail-fast rule only
// for callbacks that explicitly report committed mutation.
if (error is StreamingMutationException
{
MutationCommitted: true,
})
{
_publications.Remove(result);
}
throw;
}
});
if (!ran)
{
(_publishBeforeSpatialCommit
?? throw new InvalidOperationException(
"The presentation pipeline has no publication owners."))(
transaction.Build,
transaction.MeshData);
}
catch (Exception error)
{
// Compatibility callbacks cannot report an exact retained
// receipt. Preserve the established fail-fast rule only
// for callbacks that explicitly report committed mutation.
if (error is StreamingMutationException { MutationCommitted: true })
_publications.Remove(result);
throw;
return new LandblockPublicationAdvance(false, progressed);
}
}
transaction.PresentationCommitted = true;
@ -387,46 +749,70 @@ public sealed class LandblockPresentationPipeline
// boundary: bucket mutation, pin/script reconciliation, then the
// outer spatial observer commit. If pinning throws, the receipt is
// retained and only that suffix retries.
using GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
if (!transaction.SpatialCommitted)
{
IEnumerable<ulong>? renderIds =
transaction.Build.EnvCells?.Shells.Select(static shell => shell.GeometryId);
transaction.SpatialPublication = transaction.Kind switch
{
PublicationKind.Loaded
or PublicationKind.PromoteSelfContained
or PublicationKind.Far =>
_state.CommitLandblockSpatial(
transaction.Build.Landblock,
renderIds,
transaction.Tier),
PublicationKind.PromoteExisting =>
_state.CommitEntitiesToExistingLandblockSpatial(
transaction.LandblockId,
transaction.Build.Landblock.Entities,
renderIds),
_ => throw new InvalidOperationException(
$"Unknown landblock publication kind {transaction.Kind}."),
};
transaction.SpatialCommitted = true;
}
if (!TryRun(
default,
"publication-spatial-commit",
() =>
{
using GpuWorldState.MutationBatch mutation =
_state.BeginMutationBatch();
if (!transaction.SpatialCommitted)
{
IEnumerable<ulong>? renderIds =
transaction.Build.EnvCells?.Shells.Select(
static shell => shell.GeometryId);
transaction.SpatialPublication = transaction.Kind switch
{
PublicationKind.Loaded
or PublicationKind.PromoteSelfContained
or PublicationKind.Far =>
_state.CommitLandblockSpatial(
transaction.Build.Landblock,
renderIds,
transaction.Tier),
PublicationKind.PromoteExisting =>
_state.CommitEntitiesToExistingLandblockSpatial(
transaction.LandblockId,
transaction.Build.Landblock.Entities,
renderIds),
_ => throw new InvalidOperationException(
$"Unknown landblock publication kind {transaction.Kind}."),
};
transaction.SpatialCommitted = true;
}
_state.ActivateLandblockPresentation(
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
transaction.SpatialPresentationCommitted = true;
_state.ActivateLandblockPresentation(
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
transaction.SpatialPresentationCommitted = true;
}))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
if (!transaction.EnvCellReplayCommitted)
{
if (transaction.Kind != PublicationKind.Far
&& transaction.Build.EnvCells is { Shells.Length: > 0 } envCells)
if (!TryRun(
new StreamingWorkCost(
EntityOperations: transaction.Cost.EnvCellShells),
"publication-envcell-replay",
() =>
{
if (transaction.Kind != PublicationKind.Far
&& transaction.Build.EnvCells is
{
Shells.Length: > 0,
} envCells)
{
_ensureEnvCellMeshes?.Invoke(envCells);
}
transaction.EnvCellReplayCommitted = true;
}))
{
_ensureEnvCellMeshes?.Invoke(envCells);
return new LandblockPublicationAdvance(false, progressed);
}
transaction.EnvCellReplayCommitted = true;
}
if (!transaction.LiveRecoveryCommitted)
@ -434,10 +820,20 @@ public sealed class LandblockPresentationPipeline
// Retained live projections can materialize only after the
// canonical bucket exists. Landblock load never reconstructs their
// identity or replays create-time resources.
_onLandblockLoaded?.Invoke(transaction.LandblockId);
transaction.LiveRecoveryCommitted = true;
if (!TryRun(
default,
"publication-live-recovery",
() =>
{
_onLandblockLoaded?.Invoke(transaction.LandblockId);
transaction.LiveRecoveryCommitted = true;
}))
{
return new LandblockPublicationAdvance(false, progressed);
}
}
_publications.Remove(result);
return new LandblockPublicationAdvance(true, progressed);
}
}

View file

@ -24,7 +24,9 @@ public sealed class LandblockRenderPublication
Vector3 origin,
Dictionary<uint, LoadedCell> visibilityCells,
Vector3 aabbMin,
Vector3 aabbMax)
Vector3 aabbMax,
BuildingRegistryPublication? buildingPublication,
EnvCellLandblockPublication? envCellPublication)
{
Owner = owner;
Build = build;
@ -33,6 +35,8 @@ public sealed class LandblockRenderPublication
_visibilityCells = new ReadOnlyDictionary<uint, LoadedCell>(visibilityCells);
AabbMin = aabbMin;
AabbMax = aabbMax;
BuildingPublication = buildingPublication;
EnvCellPublication = envCellPublication;
}
internal object Owner { get; }
@ -40,6 +44,8 @@ public sealed class LandblockRenderPublication
internal LandblockMeshData MeshData { get; }
internal Vector3 AabbMin { get; }
internal Vector3 AabbMax { get; }
internal BuildingRegistryPublication? BuildingPublication { get; }
internal EnvCellLandblockPublication? EnvCellPublication { get; }
internal bool TerrainCommitted { get; set; }
internal bool VisibilityCommitted { get; set; }
internal bool AabbCommitted { get; set; }
@ -92,6 +98,7 @@ public sealed class LandblockRenderPublisher
private readonly CellVisibility _cellVisibility;
private readonly GpuWorldState _worldState;
private readonly Action<EnvCellLandblockBuild>? _commitEnvCells;
private readonly IEnvCellLandblockPublisher? _envCellPublisher;
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
private readonly Action<uint>? _removeEnvCells;
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
@ -114,7 +121,8 @@ public sealed class LandblockRenderPublisher
GpuWorldState worldState,
Action<EnvCellLandblockBuild>? commitEnvCells = null,
Action<EnvCellLandblockBuild>? prepareEnvCells = null,
Action<uint>? removeEnvCells = null)
Action<uint>? removeEnvCells = null,
IEnvCellLandblockPublisher? envCellPublisher = null)
{
ArgumentNullException.ThrowIfNull(publishTerrain);
ArgumentNullException.ThrowIfNull(removeTerrain);
@ -126,6 +134,13 @@ public sealed class LandblockRenderPublisher
_cellVisibility = cellVisibility;
_worldState = worldState;
_commitEnvCells = commitEnvCells;
_envCellPublisher = envCellPublisher;
if (_commitEnvCells is not null && _envCellPublisher is not null)
{
throw new ArgumentException(
"Supply either the retained EnvCell publisher or the compatibility callback, not both.",
nameof(envCellPublisher));
}
_prepareEnvCells = prepareEnvCells;
_removeEnvCells = removeEnvCells;
}
@ -197,6 +212,17 @@ public sealed class LandblockRenderPublisher
visibilityCells[cell.CellId] = cell;
}
(Vector3 aabbMin, Vector3 aabbMax) = ComputeAabb(meshData, origin);
BuildingRegistryPublication? buildingPublication =
build.Landblock.PhysicsDats?.Info is { } info
? BuildingLoader.PreparePublication(
info,
landblockId,
visibilityCells)
: null;
EnvCellLandblockPublication? envCellPublication =
build.EnvCells is { } retainedEnvCells
? _envCellPublisher?.PreparePublication(retainedEnvCells)
: null;
return new LandblockRenderPublication(
_receiptOwner,
build,
@ -204,7 +230,9 @@ public sealed class LandblockRenderPublisher
origin,
visibilityCells,
aabbMin,
aabbMax);
aabbMax,
buildingPublication,
envCellPublication);
}
/// <summary>
@ -214,13 +242,25 @@ public sealed class LandblockRenderPublisher
public void BeginPublication(LandblockRenderPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return;
while (!AdvanceBeginOne(publication))
{
}
}
long beginStarted = Stopwatch.GetTimestamp();
/// <summary>
/// Advances one exact render-prefix mutation from the retained receipt.
/// Terrain, visibility, and AABB publication remain ordered and retries do
/// not replay an already committed prefix.
/// </summary>
internal bool AdvanceBeginOne(LandblockRenderPublication publication)
{
ValidateReceipt(publication);
if (publication.BeginCommitted)
return true;
long started = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
uint landblockId = publication.LandblockId;
if (!publication.TerrainCommitted)
{
long terrainStarted = Stopwatch.GetTimestamp();
@ -228,15 +268,13 @@ public sealed class LandblockRenderPublisher
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
publication.TerrainCommitted = true;
}
if (!publication.VisibilityCommitted)
else if (!publication.VisibilityCommitted)
{
if (build.EnvCells is { } envCells)
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
publication.VisibilityCommitted = true;
}
if (!publication.AabbCommitted)
else if (!publication.AabbCommitted)
{
_worldState.SetLandblockAabb(
landblockId,
@ -244,10 +282,14 @@ public sealed class LandblockRenderPublisher
publication.AabbMax);
publication.AabbCommitted = true;
}
else
{
publication.BeginCommitted = true;
_beginCount++;
}
publication.BeginCommitted = true;
_beginCount++;
_beginPublishTicks += Stopwatch.GetTimestamp() - beginStarted;
_beginPublishTicks += Stopwatch.GetTimestamp() - started;
return publication.BeginCommitted;
}
/// <summary>
@ -256,40 +298,77 @@ public sealed class LandblockRenderPublisher
/// receives the complete immutable transaction.
/// </summary>
public void CompletePublication(LandblockRenderPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Render publication cannot complete before its prefix commits.");
while (!AdvanceCompleteOne(publication))
{
}
}
/// <summary>
/// Advances one exact render-suffix mutation. The building registry is
/// visible before the EnvCell renderer snapshot, matching the existing
/// retail-rooted ordering.
/// </summary>
internal bool AdvanceCompleteOne(LandblockRenderPublication publication)
{
ValidateReceipt(publication);
if (!publication.BeginCommitted)
throw new InvalidOperationException(
"Render publication cannot complete before its prefix commits.");
if (publication.CompletionCommitted)
return;
return true;
long started = Stopwatch.GetTimestamp();
LandblockBuild build = publication.Build;
uint landblockId = publication.LandblockId;
if (!publication.BuildingRegistryCommitted)
if (publication.BuildingPublication is { PreparationCommitted: false }
buildingPublication)
{
if (build.Landblock.PhysicsDats?.Info is { } info)
BuildingLoader.AdvancePreparationOne(buildingPublication);
}
else if (!publication.BuildingRegistryCommitted)
{
if (publication.BuildingPublication is { } completedBuildings)
{
BuildingLoader.CommitPublication(completedBuildings);
uint registryKey = landblockId & 0xFFFF0000u;
_buildingRegistries[registryKey] = BuildingLoader.Build(
info,
landblockId,
publication.VisibilityCells);
_buildingRegistries[registryKey] =
completedBuildings.Registry;
}
publication.BuildingRegistryCommitted = true;
}
if (!publication.EnvCellsCommitted)
else if (publication.EnvCellPublication is { } envCellPublication
&& !envCellPublication.PreparationCommitted)
{
if (build.EnvCells is { } envCells)
_envCellPublisher?.AdvancePreparationOne(envCellPublication);
}
else if (!publication.EnvCellsCommitted)
{
if (publication.EnvCellPublication is { } retained)
{
(_envCellPublisher
?? throw new InvalidOperationException(
"A retained EnvCell receipt has no owning publisher."))
.CommitPublication(retained);
}
else if (build.EnvCells is { } envCells)
{
_commitEnvCells?.Invoke(envCells);
}
publication.EnvCellsCommitted = true;
}
else
{
publication.CompletionCommitted = true;
_completeCount++;
}
publication.CompletionCommitted = true;
_completeCount++;
_completePublishTicks += Stopwatch.GetTimestamp() - started;
return publication.CompletionCommitted;
}
/// <summary>

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)

View file

@ -29,18 +29,7 @@ internal readonly record struct StreamingQueuedCompletion(
StreamingCompletionPriority Priority,
ulong Generation,
long Sequence,
long EnqueuedTimestamp)
{
public StreamingWorkCost AdmissionCost => new(
CompletionAdmissions: Estimate.Work.CompletionAdmissions,
AdoptedCpuBytes: Estimate.Work.AdoptedCpuBytes);
public StreamingWorkCost ExecutionCost => new(
EntityOperations: Estimate.Work.EntityOperations,
GpuUploadBytes: Estimate.Work.GpuUploadBytes,
GlRetireOperations:
Result is LandblockStreamResult.Unloaded ? 1 : 0);
}
long EnqueuedTimestamp);
internal readonly record struct StreamingCompletionQueueSnapshot(
int Count,

View file

@ -370,7 +370,11 @@ public sealed class StreamingController : IStreamingFrameBackend
// the desired-tier snapshot below is computed. Otherwise a landblock
// that becomes spatially resident during convergence is absent from
// the reconfiguration mutation ledger.
ConvergePendingPublications();
if (!ConvergePendingPublications())
{
_deferredRadiiRequest = (nearRadius, farRadius);
return;
}
if (_collapsed || _region is null)
{
@ -490,6 +494,12 @@ public sealed class StreamingController : IStreamingFrameBackend
_presentation.GetPendingPublicationResults();
if (pending.Count == 0)
return true;
if (_activeWorkMeter is null)
{
// Settings/native callbacks may request policy changes between
// frames, but publication remains owned by the frame-scoped meter.
return false;
}
try
{
@ -497,15 +507,14 @@ public sealed class StreamingController : IStreamingFrameBackend
for (int i = 0; i < pending.Count; i++)
{
LandblockStreamResult result = pending[i];
if (!TryObserveResultOperation(
result,
"publication-retry",
() => _presentation.ResumePublication(result),
ensureProgress: !progressed))
{
LandblockPublicationAdvance advance =
_presentation.ResumePublication(
result,
_activeWorkMeter,
ensureProgress: !progressed);
progressed |= advance.Progressed;
if (!advance.Completed)
return false;
}
progressed = true;
}
return true;
}
@ -1487,23 +1496,16 @@ public sealed class StreamingController : IStreamingFrameBackend
StreamingQueuedCompletion work = completion
?? throw new InvalidOperationException(
"The completion queue returned a null head.");
StreamingWorkCost executionCost =
IsStaleGeneration(work.Result)
? default
: work.ExecutionCost;
StreamingWorkAdmission admission = meter.TryReserve(
executionCost,
$"execute-{work.Priority}",
ensureProgress: !executed);
if (admission == StreamingWorkAdmission.Yielded)
break;
try
{
ApplyResult(work.Result);
LandblockPublicationAdvance advance = ApplyResult(
work,
meter,
ensureProgress: !executed);
executed |= advance.Progressed;
if (!advance.Completed)
break;
_completionQueue.RemoveHead(work);
meter.Complete();
executed = true;
}
catch
{
@ -1512,7 +1514,6 @@ public sealed class StreamingController : IStreamingFrameBackend
// receipt cannot safely replay, so consume only that result.
if (!_presentation.HasPendingPublication(work.Result))
_completionQueue.RemoveHead(work);
meter.Fail();
throw;
}
}
@ -1625,39 +1626,24 @@ public sealed class StreamingController : IStreamingFrameBackend
};
}
private bool TryObserveResultOperation(
LandblockStreamResult result,
private static LandblockPublicationAdvance RunSimpleResultOperation(
StreamingWorkMeter meter,
string stage,
Action operation,
StreamingWorkCost? knownCost = null,
bool ensureProgress = false)
bool ensureProgress,
Action operation)
{
StreamingWorkMeter? meter = _activeWorkMeter;
if (meter is null)
{
operation();
return true;
}
StreamingWorkCost aggregate = knownCost
?? LandblockStreamResultCost.Estimate(result).Work;
StreamingWorkCost cost = new(
EntityOperations: aggregate.EntityOperations,
GpuUploadBytes: aggregate.GpuUploadBytes,
GlRetireOperations:
result is LandblockStreamResult.Unloaded ? 1 : 0);
StreamingWorkAdmission admission = meter.TryReserve(
cost,
default,
stage,
ensureProgress);
if (admission == StreamingWorkAdmission.Yielded)
return false;
return new LandblockPublicationAdvance(false, false);
try
{
operation();
meter.Complete();
return true;
return new LandblockPublicationAdvance(true, true);
}
catch
{
@ -1671,13 +1657,22 @@ public sealed class StreamingController : IStreamingFrameBackend
/// effects: terrain upload, GPU state, and the re-hydration callback.
/// All priority queues route through this one publication path.
/// </summary>
private void ApplyResult(LandblockStreamResult result)
private LandblockPublicationAdvance ApplyResult(
StreamingQueuedCompletion work,
StreamingWorkMeter meter,
bool ensureProgress)
{
LandblockStreamResult result = work.Result;
if (_presentation.HasPendingPublication(result))
{
_presentation.ResumePublication(result);
ReconcileCompletedPendingPublication(result.LandblockId);
return;
LandblockPublicationAdvance resumed =
_presentation.ResumePublication(
result,
meter,
ensureProgress);
if (resumed.Completed)
ReconcileCompletedPendingPublication(result.LandblockId);
return resumed;
}
if (IsStaleGeneration(result))
@ -1685,7 +1680,11 @@ public sealed class StreamingController : IStreamingFrameBackend
// A worker can finish one old load/unload after a hard recenter.
// Landblock id membership cannot distinguish overlapping windows;
// generation is the logical streaming incarnation boundary.
return;
return RunSimpleResultOperation(
meter,
"execute-stale-generation",
ensureProgress,
static () => { });
}
if (result is LandblockStreamResult.Unloaded
@ -1695,7 +1694,11 @@ public sealed class StreamingController : IStreamingFrameBackend
// rapid away->back can therefore re-own an id while its same-
// generation unload is already in the worker outbox. Current
// region ownership wins; the old unload must not tear it down.
return;
return RunSimpleResultOperation(
meter,
"execute-reowned-unload",
ensureProgress,
static () => { });
}
if (result is LandblockStreamResult.Loaded
@ -1707,7 +1710,11 @@ public sealed class StreamingController : IStreamingFrameBackend
// A hard recenter/collapse can leave one worker job already in
// flight. Its completion belongs to the old region and must not
// resurrect a landblock the new StreamingRegion never owns.
return;
return RunSimpleResultOperation(
meter,
"execute-undesired-load",
ensureProgress,
static () => { });
}
bool isNearCompletion = result is LandblockStreamResult.Promoted
@ -1722,7 +1729,11 @@ public sealed class StreamingController : IStreamingFrameBackend
// high-priority jobs. Publication is idempotent at this owner:
// never append statics, replay defaults, or reapply the complete
// Near transaction to an already-Near landblock.
return;
return RunSimpleResultOperation(
meter,
"execute-duplicate-near",
ensureProgress,
static () => { });
}
if (desiredTier == LandblockStreamTier.Far && isNearCompletion)
{
@ -1739,14 +1750,26 @@ public sealed class StreamingController : IStreamingFrameBackend
switch (result)
{
case LandblockStreamResult.Loaded loaded:
_presentation.PublishAsFar(loaded, loaded.Build, loaded.MeshData);
break;
return _presentation.PublishAsFar(
loaded,
loaded.Build,
loaded.MeshData,
meter,
ensureProgress);
case LandblockStreamResult.Promoted promoted:
_presentation.PublishAsFar(promoted, promoted.Build, promoted.MeshData);
break;
return _presentation.PublishAsFar(
promoted,
promoted.Build,
promoted.MeshData,
meter,
ensureProgress);
}
}
return;
return RunSimpleResultOperation(
meter,
"execute-already-far",
ensureProgress,
static () => { });
}
}
@ -1759,31 +1782,54 @@ public sealed class StreamingController : IStreamingFrameBackend
// This Far completion was queued before a newer Near load
// or promotion. Applying it would erase the entity/cell
// layer that now owns the landblock.
break;
return RunSimpleResultOperation(
meter,
"execute-stale-far-tier",
ensureProgress,
static () => { });
}
_presentation.PublishLoaded(loaded);
break;
return _presentation.PublishLoaded(
loaded,
work.Estimate,
meter,
ensureProgress);
case LandblockStreamResult.Promoted promoted:
// PromoteToNear carries a complete build and mesh because the
// streamer deliberately lets it supersede a queued LoadFar. If
// that Far job never started, publish this as the real Near
// landblock; if the base is already resident, merge only the
// Near layer so existing live projections retain identity.
_presentation.PublishPromoted(
return _presentation.PublishPromoted(
promoted,
mergeIntoExistingLandblock: _state.IsLoaded(promoted.LandblockId));
break;
mergeIntoExistingLandblock:
_state.IsLoaded(promoted.LandblockId),
work.Estimate,
meter,
ensureProgress);
case LandblockStreamResult.Unloaded unloaded:
_presentation.EnqueueFullRetirement(unloaded.LandblockId);
break;
return RunSimpleResultOperation(
meter,
"execute-unload",
ensureProgress,
() => _presentation.EnqueueFullRetirement(
unloaded.LandblockId));
case LandblockStreamResult.Failed failed:
Console.WriteLine(
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}");
break;
return RunSimpleResultOperation(
meter,
"execute-load-failure",
ensureProgress,
() => Console.WriteLine(
$"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}"));
case LandblockStreamResult.WorkerCrashed crashed:
Console.WriteLine(
$"streaming: worker CRASHED: {crashed.Error}");
break;
return RunSimpleResultOperation(
meter,
"execute-worker-crash",
ensureProgress,
() => Console.WriteLine(
$"streaming: worker CRASHED: {crashed.Error}"));
default:
throw new InvalidOperationException(
$"Unsupported streaming result {result.GetType().Name}.");
}
}

View file

@ -455,6 +455,18 @@ public sealed class ShadowObjectRegistry
/// cells hydrated after a server spawn landed.
/// </summary>
public void RefloodLandblock(uint landblockId)
{
uint[] owners = CaptureRefloodOwnersForLandblock(landblockId);
for (int i = 0; i < owners.Length; i++)
RefloodOwnerForLandblock(owners[i], landblockId);
}
/// <summary>
/// Captures the stable ordered owner set touched by one landblock reflood.
/// App-layer streaming can retain this receipt and advance one owner per
/// frame without changing the collision registry's ownership rules.
/// </summary>
public uint[] CaptureRefloodOwnersForLandblock(uint landblockId)
{
uint lbPrefix = landblockId & 0xFFFF0000u;
var toReflood = new HashSet<uint>();
@ -487,39 +499,81 @@ public sealed class ShadowObjectRegistry
}
}
foreach (uint entityId in toReflood)
uint[] ordered = toReflood.ToArray();
Array.Sort(ordered);
return ordered;
}
/// <summary>
/// Re-runs one owner from a retained landblock-reflood receipt. A removed,
/// suspended, or otherwise superseded owner is an idempotent no-op.
/// </summary>
public void RefloodOwnerForLandblock(uint entityId, uint landblockId)
{
uint lbPrefix = landblockId & 0xFFFF0000u;
if (_suspendedEntities.Contains(entityId)
|| !_entityReg.TryGetValue(
entityId,
out RegistrationRecord? reg))
{
var reg = _entityReg[entityId];
_withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawnBeforeReflood);
if (reg.IsMultiPart && _entityShapes.TryGetValue(entityId, out var shapes))
{
RegisterMultiPart(entityId, reg.EntityWorldPos, reg.EntityWorldRot, shapes,
reg.State, reg.Flags, 0f, 0f, lbPrefix,
reg.SeedCellId, reg.IsStatic);
}
else
{
Register(entityId, reg.GfxObjId, reg.EntityWorldPos, reg.EntityWorldRot,
reg.Radius, 0f, 0f, lbPrefix,
reg.CollisionType, reg.CylHeight, reg.Scale,
reg.State, reg.Flags, reg.SeedCellId, reg.IsStatic);
}
return;
}
// Register is also the authoritative movement/replacement API and
// therefore clears obsolete markers. Only this streaming reflood
// operation preserves the still-missing prefixes across replacement.
if (withdrawnBeforeReflood is not null)
_withdrawnPrefixesByOwner[entityId] = withdrawnBeforeReflood;
_withdrawnPrefixesByOwner.TryGetValue(
entityId,
out var withdrawnBeforeReflood);
if (reg.IsMultiPart
&& _entityShapes.TryGetValue(entityId, out var shapes))
{
RegisterMultiPart(
entityId,
reg.EntityWorldPos,
reg.EntityWorldRot,
shapes,
reg.State,
reg.Flags,
0f,
0f,
lbPrefix,
reg.SeedCellId,
reg.IsStatic);
}
else
{
Register(
entityId,
reg.GfxObjId,
reg.EntityWorldPos,
reg.EntityWorldRot,
reg.Radius,
0f,
0f,
lbPrefix,
reg.CollisionType,
reg.CylHeight,
reg.Scale,
reg.State,
reg.Flags,
reg.SeedCellId,
reg.IsStatic);
}
if (_entityToCells.TryGetValue(entityId, out var refreshedCells)
&& refreshedCells.Exists(cell =>
(cell & 0xFFFF0000u) == lbPrefix)
&& _withdrawnPrefixesByOwner.TryGetValue(entityId, out var withdrawn))
{
withdrawn.Remove(lbPrefix);
if (withdrawn.Count == 0)
_withdrawnPrefixesByOwner.Remove(entityId);
}
// Register is also the authoritative movement/replacement API and
// therefore clears obsolete markers. Only this streaming reflood
// operation preserves the still-missing prefixes across replacement.
if (withdrawnBeforeReflood is not null)
_withdrawnPrefixesByOwner[entityId] = withdrawnBeforeReflood;
if (_entityToCells.TryGetValue(entityId, out var refreshedCells)
&& refreshedCells.Exists(cell =>
(cell & 0xFFFF0000u) == lbPrefix)
&& _withdrawnPrefixesByOwner.TryGetValue(
entityId,
out var withdrawn))
{
withdrawn.Remove(lbPrefix);
if (withdrawn.Count == 0)
_withdrawnPrefixesByOwner.Remove(entityId);
}
}
@ -596,6 +650,17 @@ public sealed class ShadowObjectRegistry
/// are deliberately retained for spatial reflood after streaming changes.
/// </summary>
public void DeregisterStaticOwnersForLandblock(uint landblockId)
{
uint[] owners = CaptureStaticOwnersForLandblock(landblockId);
for (int i = 0; i < owners.Length; i++)
DeregisterStaticOwnerForLandblock(owners[i], landblockId);
}
/// <summary>
/// Captures a stable ordered receipt for static collision owners rooted in
/// one landblock.
/// </summary>
public uint[] CaptureStaticOwnersForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
var owners = new List<uint>();
@ -607,9 +672,27 @@ public sealed class ShadowObjectRegistry
owners.Add(entityId);
}
}
owners.Sort();
return owners.ToArray();
}
foreach (uint entityId in owners)
/// <summary>
/// Removes one static owner from a retained landblock receipt. If the
/// owner was already removed or rebound elsewhere, the operation no-ops.
/// </summary>
public void DeregisterStaticOwnerForLandblock(
uint entityId,
uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
if (_entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration)
&& registration.IsStatic
&& (registration.SeedCellId & 0xFFFF0000u) == prefix)
{
Deregister(entityId);
}
}
/// <summary>