290 lines
10 KiB
C#
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));
|
|
}
|
|
}
|