refactor(streaming): extract landblock render publisher
This commit is contained in:
parent
0f9c03f422
commit
1029f8372e
4 changed files with 708 additions and 128 deletions
290
src/AcDream.App/Streaming/LandblockRenderPublisher.cs
Normal file
290
src/AcDream.App/Streaming/LandblockRenderPublisher.cs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Terrain;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable receipt for the render-owned prefix of one accepted landblock
|
||||
/// publication. Physics and static presentation consume the same captured
|
||||
/// origin between <see cref="LandblockRenderPublisher.BeginPublication"/> and
|
||||
/// <see cref="LandblockRenderPublisher.CompletePublication"/>.
|
||||
/// </summary>
|
||||
public sealed class LandblockRenderPublication
|
||||
{
|
||||
private readonly IReadOnlyDictionary<uint, LoadedCell> _visibilityCells;
|
||||
|
||||
internal LandblockRenderPublication(
|
||||
object owner,
|
||||
LandblockBuild build,
|
||||
Vector3 origin,
|
||||
Dictionary<uint, LoadedCell> visibilityCells)
|
||||
{
|
||||
Owner = owner;
|
||||
Build = build;
|
||||
Origin = origin;
|
||||
_visibilityCells = new ReadOnlyDictionary<uint, LoadedCell>(visibilityCells);
|
||||
}
|
||||
|
||||
internal object Owner { get; }
|
||||
internal LandblockBuild Build { get; }
|
||||
internal bool CompletionCommitted { get; set; }
|
||||
|
||||
public uint LandblockId => Build.Landblock.LandblockId;
|
||||
public Vector3 Origin { get; }
|
||||
public IReadOnlyDictionary<uint, LoadedCell> VisibilityCells => _visibilityCells;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed cumulative diagnostics owned by the render publication boundary.
|
||||
/// Times use <see cref="Stopwatch"/> ticks so callers can aggregate without
|
||||
/// losing sub-millisecond precision.
|
||||
/// </summary>
|
||||
public readonly record struct LandblockRenderPublisherDiagnostics(
|
||||
long BeginCount,
|
||||
long CompleteCount,
|
||||
long TerrainPublishTicks,
|
||||
long BeginPublishTicks,
|
||||
long CompletePublishTicks,
|
||||
long EnvCellPreparationCount,
|
||||
long TerrainRemovalCount,
|
||||
long CellVisibilityRemovalCount,
|
||||
long BuildingRegistryRemovalCount,
|
||||
long EnvCellRemovalCount);
|
||||
|
||||
/// <summary>
|
||||
/// Update-thread owner of landblock render publication. It publishes terrain,
|
||||
/// the complete visibility-cell transaction, frustum bounds, building
|
||||
/// registries, and EnvCell renderer state without reading DATs or deciding
|
||||
/// residency. Render-ID acquisition remains in <see cref="GpuWorldState"/>;
|
||||
/// schema-aware EnvCell preparation is replayed only after that acquisition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ordering preserves retail <c>LScape::grab_visible_cells</c>
|
||||
/// (0x00504EC0), <c>CLandBlock::init_buildings</c> (0x0052FD80),
|
||||
/// <c>CLandBlock::grab_visible_cells</c> (0x0052F460), and the extracted
|
||||
/// WorldBuilder one-job/one-EnvCell-transaction seam documented in
|
||||
/// <c>docs/architecture/worldbuilder-inventory.md</c>.
|
||||
/// </remarks>
|
||||
public sealed class LandblockRenderPublisher
|
||||
{
|
||||
private readonly object _receiptOwner = new();
|
||||
private readonly Action<uint, LandblockMeshData, Vector3> _publishTerrain;
|
||||
private readonly Action<uint> _removeTerrain;
|
||||
private readonly CellVisibility _cellVisibility;
|
||||
private readonly GpuWorldState _worldState;
|
||||
private readonly Action<EnvCellLandblockBuild>? _commitEnvCells;
|
||||
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
|
||||
private readonly Action<uint>? _removeEnvCells;
|
||||
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
|
||||
|
||||
private long _beginCount;
|
||||
private long _completeCount;
|
||||
private long _terrainPublishTicks;
|
||||
private long _beginPublishTicks;
|
||||
private long _completePublishTicks;
|
||||
private long _envCellPreparationCount;
|
||||
private long _terrainRemovalCount;
|
||||
private long _cellVisibilityRemovalCount;
|
||||
private long _buildingRegistryRemovalCount;
|
||||
private long _envCellRemovalCount;
|
||||
|
||||
public LandblockRenderPublisher(
|
||||
Action<uint, LandblockMeshData, Vector3> publishTerrain,
|
||||
Action<uint> removeTerrain,
|
||||
CellVisibility cellVisibility,
|
||||
GpuWorldState worldState,
|
||||
Action<EnvCellLandblockBuild>? commitEnvCells = null,
|
||||
Action<EnvCellLandblockBuild>? prepareEnvCells = null,
|
||||
Action<uint>? removeEnvCells = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(publishTerrain);
|
||||
ArgumentNullException.ThrowIfNull(removeTerrain);
|
||||
ArgumentNullException.ThrowIfNull(cellVisibility);
|
||||
ArgumentNullException.ThrowIfNull(worldState);
|
||||
|
||||
_publishTerrain = publishTerrain;
|
||||
_removeTerrain = removeTerrain;
|
||||
_cellVisibility = cellVisibility;
|
||||
_worldState = worldState;
|
||||
_commitEnvCells = commitEnvCells;
|
||||
_prepareEnvCells = prepareEnvCells;
|
||||
_removeEnvCells = removeEnvCells;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<BuildingRegistry> BuildingRegistries =>
|
||||
_buildingRegistries.Values;
|
||||
|
||||
public LandblockRenderPublisherDiagnostics Diagnostics => new(
|
||||
_beginCount,
|
||||
_completeCount,
|
||||
_terrainPublishTicks,
|
||||
_beginPublishTicks,
|
||||
_completePublishTicks,
|
||||
_envCellPreparationCount,
|
||||
_terrainRemovalCount,
|
||||
_cellVisibilityRemovalCount,
|
||||
_buildingRegistryRemovalCount,
|
||||
_envCellRemovalCount);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the prefix that retail establishes before physics/static
|
||||
/// activation: terrain, complete visibility cells, and spatial bounds.
|
||||
/// </summary>
|
||||
public LandblockRenderPublication BeginPublication(
|
||||
LandblockBuild build,
|
||||
LandblockMeshData meshData)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(build);
|
||||
ArgumentNullException.ThrowIfNull(meshData);
|
||||
if (!build.Origin.IsSpecified)
|
||||
throw new ArgumentException(
|
||||
"Render publication requires the build's captured origin.",
|
||||
nameof(build));
|
||||
|
||||
long beginStarted = Stopwatch.GetTimestamp();
|
||||
uint landblockId = build.Landblock.LandblockId;
|
||||
if (build.EnvCells is { } candidateEnvCells &&
|
||||
candidateEnvCells.LandblockId != landblockId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"EnvCell transaction 0x{candidateEnvCells.LandblockId:X8} was attached to " +
|
||||
$"landblock 0x{landblockId:X8}.");
|
||||
}
|
||||
|
||||
Vector3 origin = ComputeOrigin(landblockId, build.Origin);
|
||||
|
||||
long terrainStarted = Stopwatch.GetTimestamp();
|
||||
_publishTerrain(landblockId, meshData, origin);
|
||||
_terrainPublishTicks += Stopwatch.GetTimestamp() - terrainStarted;
|
||||
|
||||
var committedCells = new Dictionary<uint, LoadedCell>();
|
||||
if (build.EnvCells is { } envCells)
|
||||
{
|
||||
_cellVisibility.CommitLandblock(landblockId, envCells.VisibilityCells);
|
||||
foreach (LoadedCell cell in envCells.VisibilityCells)
|
||||
committedCells[cell.CellId] = cell;
|
||||
}
|
||||
|
||||
(Vector3 aabbMin, Vector3 aabbMax) = ComputeAabb(meshData, origin);
|
||||
_worldState.SetLandblockAabb(landblockId, aabbMin, aabbMax);
|
||||
|
||||
_beginCount++;
|
||||
_beginPublishTicks += Stopwatch.GetTimestamp() - beginStarted;
|
||||
return new LandblockRenderPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin,
|
||||
committedCells);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the render-owned suffix after physics has consumed the same
|
||||
/// receipt: building membership is stamped before the EnvCell renderer
|
||||
/// receives the complete immutable transaction.
|
||||
/// </summary>
|
||||
public void CompletePublication(LandblockRenderPublication publication)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(publication);
|
||||
if (!ReferenceEquals(publication.Owner, _receiptOwner))
|
||||
throw new ArgumentException(
|
||||
"The render publication receipt belongs to another publisher.",
|
||||
nameof(publication));
|
||||
if (publication.CompletionCommitted)
|
||||
return;
|
||||
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
LandblockBuild build = publication.Build;
|
||||
uint landblockId = publication.LandblockId;
|
||||
if (build.Landblock.PhysicsDats?.Info is { } info)
|
||||
{
|
||||
uint registryKey = landblockId & 0xFFFF0000u;
|
||||
_buildingRegistries[registryKey] = BuildingLoader.Build(
|
||||
info,
|
||||
landblockId,
|
||||
publication.VisibilityCells);
|
||||
}
|
||||
|
||||
if (build.EnvCells is { } envCells)
|
||||
_commitEnvCells?.Invoke(envCells);
|
||||
|
||||
publication.CompletionCommitted = true;
|
||||
_completeCount++;
|
||||
_completePublishTicks += Stopwatch.GetTimestamp() - started;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replays EnvCell schema preparation after <see cref="GpuWorldState"/>
|
||||
/// has acquired every synthetic render identifier.
|
||||
/// </summary>
|
||||
public void PrepareAfterRenderPins(EnvCellLandblockBuild build)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(build);
|
||||
_prepareEnvCells?.Invoke(build);
|
||||
_envCellPreparationCount++;
|
||||
}
|
||||
|
||||
public void RemoveTerrain(uint landblockId)
|
||||
{
|
||||
_removeTerrain(landblockId);
|
||||
_terrainRemovalCount++;
|
||||
}
|
||||
|
||||
public void RemoveCellVisibility(uint landblockId)
|
||||
{
|
||||
_cellVisibility.RemoveLandblock((landblockId >> 16) & 0xFFFFu);
|
||||
_cellVisibilityRemovalCount++;
|
||||
}
|
||||
|
||||
public void RemoveBuildingRegistry(uint landblockId)
|
||||
{
|
||||
_buildingRegistries.Remove(landblockId & 0xFFFF0000u);
|
||||
_buildingRegistryRemovalCount++;
|
||||
}
|
||||
|
||||
public void RemoveEnvironmentCells(uint landblockId)
|
||||
{
|
||||
_removeEnvCells?.Invoke(landblockId);
|
||||
_envCellRemovalCount++;
|
||||
}
|
||||
|
||||
private static Vector3 ComputeOrigin(
|
||||
uint landblockId,
|
||||
LandblockBuildOrigin capturedOrigin)
|
||||
{
|
||||
int landblockX = (int)((landblockId >> 24) & 0xFFu);
|
||||
int landblockY = (int)((landblockId >> 16) & 0xFFu);
|
||||
return new Vector3(
|
||||
(landblockX - capturedOrigin.CenterX) * 192f,
|
||||
(landblockY - capturedOrigin.CenterY) * 192f,
|
||||
0f);
|
||||
}
|
||||
|
||||
private static (Vector3 Min, Vector3 Max) ComputeAabb(
|
||||
LandblockMeshData meshData,
|
||||
Vector3 origin)
|
||||
{
|
||||
float zMin = float.MaxValue;
|
||||
float zMax = float.MinValue;
|
||||
foreach (TerrainVertex vertex in meshData.Vertices)
|
||||
{
|
||||
float z = vertex.Position.Z;
|
||||
if (z < zMin) zMin = z;
|
||||
if (z > zMax) zMax = z;
|
||||
}
|
||||
if (zMin == float.MaxValue)
|
||||
{
|
||||
zMin = 0f;
|
||||
zMax = 0f;
|
||||
}
|
||||
|
||||
zMax += 50f;
|
||||
zMin -= 10f;
|
||||
return (
|
||||
new Vector3(origin.X, origin.Y, zMin),
|
||||
new Vector3(origin.X + 192f, origin.Y + 192f, zMax));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue