refactor(streaming): extract landblock render publisher
This commit is contained in:
parent
0f9c03f422
commit
1029f8372e
4 changed files with 708 additions and 128 deletions
|
|
@ -151,6 +151,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
||||
private AcDream.App.Streaming.LandblockRenderPublisher? _landblockRenderPublisher;
|
||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||
|
|
@ -228,14 +229,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// mesh builder (A.5 T11+) can call LandblockMesh.Build without a lock.
|
||||
private System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.Core.Terrain.SurfaceInfo>? _surfaceCache;
|
||||
|
||||
// Phase A8 (2026-05-26): per-landblock BuildingRegistry keyed by full landblock
|
||||
// id (e.g. 0xA9B40000). Built from LandBlockInfo.Buildings at ApplyLoadedTerrain
|
||||
// time; each entry's BuildingRegistry.GetBuildingsContainingCell drives render-frame
|
||||
// indoor-cell scoping. Entries are removed in the removeTerrain callbacks.
|
||||
// Only touched on the render thread — no lock required.
|
||||
private readonly System.Collections.Generic.Dictionary<uint, AcDream.App.Rendering.Wb.BuildingRegistry>
|
||||
_buildingRegistries = new();
|
||||
|
||||
// Phase A8 (2026-05-28): WB EnvCellRenderManager port. Cells render
|
||||
// through this dedicated pipeline now, NOT through WbDrawDispatcher.
|
||||
// The dispatcher's IndoorPass still walks cell-static stabs (WorldEntity
|
||||
|
|
@ -2419,6 +2412,18 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
|
||||
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
|
||||
_envCellRenderer.Initialize(_meshShader!);
|
||||
_landblockRenderPublisher = new AcDream.App.Streaming.LandblockRenderPublisher(
|
||||
publishTerrain: (landblockId, meshData, origin) =>
|
||||
_terrain!.AddLandblockWithMesh(landblockId, meshData, origin),
|
||||
removeTerrain: landblockId => _terrain!.RemoveLandblock(landblockId),
|
||||
cellVisibility: _cellVisibility,
|
||||
worldState: _worldState,
|
||||
commitEnvCells: _envCellRenderer.CommitLandblock,
|
||||
prepareEnvCells: build =>
|
||||
AcDream.App.Rendering.Wb.EnvCellMeshPreparationScheduler.Schedule(
|
||||
build,
|
||||
_wbMeshAdapter.MeshManager!),
|
||||
removeEnvCells: _envCellRenderer.RemoveLandblock);
|
||||
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
|
|
@ -2531,7 +2536,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// objects it thinks we still know, so we re-project them ourselves.
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes: EnsureEnvCellMeshesAfterPin,
|
||||
ensureEnvCellMeshes: _landblockRenderPublisher!.PrepareAfterRenderPins,
|
||||
retirementCoordinator: _landblockRetirements);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
|
|
@ -3774,12 +3779,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
$"live: AdminEnvirons sound cue = {name} " +
|
||||
$"(0x{environChangeType:X2}) — audio binding pending");
|
||||
}
|
||||
private void EnsureEnvCellMeshesAfterPin(
|
||||
AcDream.App.Rendering.Wb.EnvCellLandblockBuild build)
|
||||
{
|
||||
if (_wbMeshAdapter?.MeshManager is { } meshManager)
|
||||
AcDream.App.Rendering.Wb.EnvCellMeshPreparationScheduler.Schedule(build, meshManager);
|
||||
}
|
||||
private void AdvanceLandblockPresentationRetirement(
|
||||
AcDream.App.Streaming.LandblockRetirementTicket ticket)
|
||||
{
|
||||
|
|
@ -3807,7 +3806,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
{
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.Terrain,
|
||||
() => _terrain?.RemoveLandblock(ticket.LandblockId));
|
||||
() => _landblockRenderPublisher?.RemoveTerrain(ticket.LandblockId));
|
||||
}
|
||||
|
||||
ticket.RunOnce(
|
||||
|
|
@ -3821,15 +3820,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
});
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.CellVisibility,
|
||||
() => _cellVisibility.RemoveLandblock(
|
||||
(ticket.LandblockId >> 16) & 0xFFFFu));
|
||||
() => _landblockRenderPublisher?.RemoveCellVisibility(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.BuildingRegistry,
|
||||
() => _buildingRegistries.Remove(
|
||||
ticket.LandblockId & 0xFFFF0000u));
|
||||
() => _landblockRenderPublisher?.RemoveBuildingRegistry(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EnvironmentCells,
|
||||
() => _envCellRenderer?.RemoveLandblock(ticket.LandblockId));
|
||||
() => _landblockRenderPublisher?.RemoveEnvironmentCells(ticket.LandblockId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3893,76 +3890,35 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
AcDream.Core.Terrain.LandblockMeshData meshData)
|
||||
{
|
||||
var lb = build.Landblock;
|
||||
// _blendCtx / _surfaceCache no longer needed here (mesh pre-built by worker).
|
||||
// _heightTable still needed for physics TerrainSurface below.
|
||||
if (_terrain is null || _dats is null || _heightTable is null) return;
|
||||
if (_landblockRenderPublisher is null || _dats is null || _heightTable is null)
|
||||
return;
|
||||
|
||||
// datLock fix: every dat the apply needs was pre-read by the worker into
|
||||
// lb.PhysicsDats. Read from the bundle — NO _dats.Get here, so this method
|
||||
// (and its caller) need no _datLock.
|
||||
// Every DAT object publication consumes was captured by the worker.
|
||||
// The update-thread publishers consume only this immutable bundle.
|
||||
var datBundle = lb.PhysicsDats ?? AcDream.Core.World.PhysicsDatBundle.Empty;
|
||||
|
||||
uint lbXu = (lb.LandblockId >> 24) & 0xFFu;
|
||||
uint lbYu = (lb.LandblockId >> 16) & 0xFFu;
|
||||
int lbX = (int)lbXu;
|
||||
int lbY = (int)lbYu;
|
||||
var origin = new System.Numerics.Vector3(
|
||||
(lbX - build.Origin.CenterX) * 192f,
|
||||
(lbY - build.Origin.CenterY) * 192f,
|
||||
0f);
|
||||
|
||||
// Phase A.5 T15/T16: route through AddLandblockWithMesh — the named
|
||||
// two-tier entry point. Delegates to AddLandblock internally; both
|
||||
// paths share one GPU upload path.
|
||||
// [FRAME-DIAG]: bracket the terrain glBufferSubData sub-span so we can tell
|
||||
// the (tiny ~17KB) GPU upload apart from the dat-read/registration tail.
|
||||
long fdU0 = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||
_terrain.AddLandblockWithMesh(lb.LandblockId, meshData, origin);
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics renderDiagBefore =
|
||||
_landblockRenderPublisher.Diagnostics;
|
||||
AcDream.App.Streaming.LandblockRenderPublication renderPublication =
|
||||
_landblockRenderPublisher.BeginPublication(build, meshData);
|
||||
var origin = renderPublication.Origin;
|
||||
if (_frameDiag)
|
||||
_applyUploadAccumTicks += System.Diagnostics.Stopwatch.GetTimestamp() - fdU0;
|
||||
{
|
||||
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics renderDiagAfter =
|
||||
_landblockRenderPublisher.Diagnostics;
|
||||
long terrainTicks =
|
||||
renderDiagAfter.TerrainPublishTicks - renderDiagBefore.TerrainPublishTicks;
|
||||
_applyUploadAccumTicks += terrainTicks;
|
||||
_applyCellAccumTicks +=
|
||||
renderDiagAfter.BeginPublishTicks
|
||||
- renderDiagBefore.BeginPublishTicks
|
||||
- terrainTicks;
|
||||
}
|
||||
|
||||
// [FRAME-DIAG]: split the post-upload apply CPU into three sub-spans via
|
||||
// method-scope checkpoints — cell-build → gfxobj-BSP → ShadowObjects+lights.
|
||||
// [FRAME-DIAG]: the publisher reports terrain and render-prefix time;
|
||||
// this checkpoint begins the physics/cache portion of the cell span.
|
||||
long fdCheck = _frameDiag ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
|
||||
|
||||
// Commit the exact visibility-cell snapshot produced by THIS streaming
|
||||
// completion. The former global ConcurrentBag let one landblock drain
|
||||
// another landblock's cells and exposed partial builds to the flood.
|
||||
var committedCells = new System.Collections.Generic.Dictionary<uint, LoadedCell>();
|
||||
if (build.EnvCells is { } envCellBuild)
|
||||
{
|
||||
if (envCellBuild.LandblockId != lb.LandblockId)
|
||||
throw new InvalidOperationException(
|
||||
$"EnvCell transaction 0x{envCellBuild.LandblockId:X8} was attached to " +
|
||||
$"landblock 0x{lb.LandblockId:X8}.");
|
||||
|
||||
_cellVisibility.CommitLandblock(lb.LandblockId, envCellBuild.VisibilityCells);
|
||||
foreach (var cell in envCellBuild.VisibilityCells)
|
||||
committedCells[cell.CellId] = cell;
|
||||
}
|
||||
|
||||
// Compute the per-landblock AABB for frustum culling. XY from the
|
||||
// landblock's world origin + 192 footprint. Z from the terrain vertex
|
||||
// range padded +50 above (for trees/buildings) and -10 below (for
|
||||
// basements). TerrainRenderer already scans vertices internally; we
|
||||
// replicate here so GpuWorldState has the same bounds for the static
|
||||
// mesh renderer's culling pass.
|
||||
{
|
||||
float zMin = float.MaxValue, zMax = float.MinValue;
|
||||
foreach (var v in meshData.Vertices)
|
||||
{
|
||||
float z = v.Position.Z;
|
||||
if (z < zMin) zMin = z;
|
||||
if (z > zMax) zMax = z;
|
||||
}
|
||||
if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
|
||||
zMax += 50f; // generous pad for trees and buildings
|
||||
zMin -= 10f; // below-ground buffer for basements/cellars
|
||||
var aabbMin = new System.Numerics.Vector3(origin.X, origin.Y, zMin);
|
||||
var aabbMax = new System.Numerics.Vector3(origin.X + 192f, origin.Y + 192f, zMax);
|
||||
_worldState.SetLandblockAabb(lb.LandblockId, aabbMin, aabbMax);
|
||||
}
|
||||
|
||||
// Phase B.3: populate the physics engine with terrain + indoor cell
|
||||
// surfaces for this landblock. Runs under _datLock (same lock as the
|
||||
// rest of ApplyLoadedTerrainLocked) so dat reads are safe.
|
||||
|
|
@ -4155,47 +4111,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
|
||||
_physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces,
|
||||
portalPlanes, origin.X, origin.Y);
|
||||
|
||||
// Phase A8 (2026-05-26 / fix 2026-05-28): build per-landblock
|
||||
// BuildingRegistry from LandBlockInfo.Buildings, stamping
|
||||
// LoadedCell.BuildingId for each cell in a building's cell set.
|
||||
//
|
||||
// FIX 2026-05-28: previously passed `drainedCells` only — that's the
|
||||
// dict of cells drained THIS frame. Cells loaded on prior frames
|
||||
// were missed, leaving their BuildingId null and the
|
||||
// `cameraInsideBuilding` gate FALSE even when the player was inside
|
||||
// a tagged cottage. (First visual-gate launch showed 8737 [vis]
|
||||
// lines with inside=True really=True but ZERO [buildings] probe
|
||||
// emissions — same root cause as the RR7.1 / RR7.2 saga.) The fix:
|
||||
// merge `drainedCells` with every cell currently registered with
|
||||
// _cellVisibility for this landblock prefix. BuildingLoader's BFS
|
||||
// + stamping pass then sees the complete cell set.
|
||||
//
|
||||
// Cells without a building stay at BuildingId == null (outdoor
|
||||
// surface cells; dungeon cells not enumerated in LandBlockInfo.Buildings).
|
||||
if (lbInfo is not null)
|
||||
{
|
||||
// The streaming result now carries the complete landblock cell
|
||||
// set; no cross-frame merge or global-bag recovery is required.
|
||||
var lbStampCells = committedCells;
|
||||
// FIX 2026-05-28: normalize storage key to the cell-prefix
|
||||
// convention (`landblockId & 0xFFFF0000u`). The lb.LandblockId
|
||||
// field encodes 0xXXYY_FFFF (low 16 bits = 0xFFFF for the
|
||||
// landblock's own LandBlockInfo dat id), but the runtime
|
||||
// lookup at line ~7110 derives the key from a cell id via
|
||||
// `cellId & 0xFFFF0000u` which yields 0xXXYY_0000. Storage
|
||||
// and lookup must agree. Mask both sides to the upper-16
|
||||
// form so the registry resolves correctly. This is the same
|
||||
// bug the RR7.2 commit (`efe3520`, reverted by `9aaae02`)
|
||||
// tried to fix; landing it here in the data-flow layer.
|
||||
uint regKey = lb.LandblockId & 0xFFFF0000u;
|
||||
_buildingRegistries[regKey] =
|
||||
AcDream.App.Rendering.Wb.BuildingLoader.Build(
|
||||
lbInfo, lb.LandblockId, lbStampCells);
|
||||
}
|
||||
|
||||
if (build.EnvCells is { } renderBuild)
|
||||
_envCellRenderer?.CommitLandblock(renderBuild);
|
||||
_landblockRenderPublisher.CompletePublication(renderPublication);
|
||||
}
|
||||
|
||||
// N.5: WbMeshAdapter.Tick() handles GPU upload for all GfxObj meshes via
|
||||
|
|
@ -5623,7 +5539,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// routes interior-root look-ins to its landscape-stage sub-pass
|
||||
// (DrawBuildingLookIns); the root's own building self-excludes
|
||||
// via the seed eye-side test.
|
||||
foreach (var registry in _buildingRegistries.Values)
|
||||
foreach (var registry in _landblockRenderPublisher?.BuildingRegistries
|
||||
?? Array.Empty<AcDream.App.Rendering.Wb.BuildingRegistry>())
|
||||
{
|
||||
foreach (var b in registry.All())
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue