perf(streaming): cursor publication across frame budgets
This commit is contained in:
parent
bb16f74fd4
commit
98f1ac8934
32 changed files with 2215 additions and 823 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
Normal file
45
src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue