acdream/src/AcDream.App/Streaming/LandblockRenderPublisher.cs

290 lines
10 KiB
C#

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));
}
}