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; /// /// Immutable receipt for the render-owned prefix of one accepted landblock /// publication. Physics and static presentation consume the same captured /// origin between and /// . /// public sealed class LandblockRenderPublication { private readonly IReadOnlyDictionary _visibilityCells; internal LandblockRenderPublication( object owner, LandblockBuild build, Vector3 origin, Dictionary visibilityCells) { Owner = owner; Build = build; Origin = origin; _visibilityCells = new ReadOnlyDictionary(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 VisibilityCells => _visibilityCells; } /// /// Typed cumulative diagnostics owned by the render publication boundary. /// Times use ticks so callers can aggregate without /// losing sub-millisecond precision. /// 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); /// /// 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 ; /// schema-aware EnvCell preparation is replayed only after that acquisition. /// /// /// The ordering preserves retail LScape::grab_visible_cells /// (0x00504EC0), CLandBlock::init_buildings (0x0052FD80), /// CLandBlock::grab_visible_cells (0x0052F460), and the extracted /// WorldBuilder one-job/one-EnvCell-transaction seam documented in /// docs/architecture/worldbuilder-inventory.md. /// public sealed class LandblockRenderPublisher { private readonly object _receiptOwner = new(); private readonly Action _publishTerrain; private readonly Action _removeTerrain; private readonly CellVisibility _cellVisibility; private readonly GpuWorldState _worldState; private readonly Action? _commitEnvCells; private readonly Action? _prepareEnvCells; private readonly Action? _removeEnvCells; private readonly Dictionary _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 publishTerrain, Action removeTerrain, CellVisibility cellVisibility, GpuWorldState worldState, Action? commitEnvCells = null, Action? prepareEnvCells = null, Action? 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 BuildingRegistries => _buildingRegistries.Values; public LandblockRenderPublisherDiagnostics Diagnostics => new( _beginCount, _completeCount, _terrainPublishTicks, _beginPublishTicks, _completePublishTicks, _envCellPreparationCount, _terrainRemovalCount, _cellVisibilityRemovalCount, _buildingRegistryRemovalCount, _envCellRemovalCount); /// /// Publishes the prefix that retail establishes before physics/static /// activation: terrain, complete visibility cells, and spatial bounds. /// 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(); 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); } /// /// 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. /// 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; } /// /// Replays EnvCell schema preparation after /// has acquired every synthetic render identifier. /// 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)); } }