refactor(render): extract typed retail pview passes

This commit is contained in:
Erik 2026-07-22 06:40:09 +02:00
parent 6d6e5b5fa5
commit 85239fb373
13 changed files with 1888 additions and 841 deletions

View file

@ -83,7 +83,6 @@ public sealed class GameWindow : IDisposable
System.Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
"1",
System.StringComparison.Ordinal);
private long _frameDiagLastPublicationMilliseconds;
// MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call
// per OnRender + three stage scopes. All logic lives in
@ -224,6 +223,10 @@ public sealed class GameWindow : IDisposable
// _interiorRenderer is constructed once both renderers exist; _interiorPartition is rebuilt
// each frame on an indoor root (null on the outdoor root).
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
private AcDream.App.Rendering.RetailPViewPassExecutor? _retailPViewPassExecutor;
private AcDream.App.Rendering.RetailPViewCellSource? _retailPViewCells;
private AcDream.App.Rendering.TerrainDrawDiagnosticsController?
_terrainDrawDiagnostics;
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
private AcDream.App.Rendering.PortalTunnelPresentation? _portalTunnel;
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
@ -237,11 +240,10 @@ public sealed class GameWindow : IDisposable
// U.4 replaces the NoClip() frame with one built from the portal-visibility result.
private ClipFrame? _clipFrame;
// PView pass-local particle classifications move with the pass executor in
// Checkpoint E; GameWindow still owns them until that cutover.
private readonly HashSet<uint> _outdoorSceneParticleEntityIds = new();
private readonly HashSet<uint> _visibleSceneParticleEntityIds = new();
private readonly HashSet<uint> _noSceneParticleEntityIds = new();
// Flat-world fallback particle scratch. PView classifications belong to
// RetailPViewPassExecutor.
private readonly HashSet<uint> _visibleSceneParticleEntityIds = [];
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
/// <summary>
/// Phase 6.4: per-entity animation playback state for entities whose
@ -2311,8 +2313,7 @@ public sealed class GameWindow : IDisposable
landblockRenderPublisher.PrepareAfterRenderPins);
_clipFrame ??= ClipFrame.NoClip();
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer();
// T1: invisible portal depth writes (seal/punch) — retail
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
@ -2892,6 +2893,37 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource(
_landblockPresentationPipeline!,
_cellVisibility));
_terrainDrawDiagnostics =
new AcDream.App.Rendering.TerrainDrawDiagnosticsController(
_frameDiag,
_worldRenderDiagnostics!,
new AcDream.App.Rendering.RuntimeFramePipelineDiagnosticFactsSource(
_terrain,
_landblockPresentationPipeline,
_renderFrameLivePreparation,
_wbDrawDispatcher,
_streamingController,
_liveEntities!,
_worldState),
_renderDiagnosticLog);
_retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource(
_cellVisibility);
_retailPViewPassExecutor =
new AcDream.App.Rendering.RetailPViewPassExecutor(
_gl!,
new AcDream.App.Rendering.SilkRetailPViewFramebufferSource(
_window!),
_clipFrame!,
_terrain,
_envCellRenderer!,
_wbDrawDispatcher!,
_skyRenderer,
_particleSystem,
_particleRenderer,
_portalDepthMask,
_retailAlphaQueue,
_worldRenderDiagnostics!,
_terrainDrawDiagnostics);
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame,
_worldState,
@ -3527,7 +3559,6 @@ public sealed class GameWindow : IDisposable
int sigOutdoorRootObjectCount = 0;
int sigLiveDynamicDrawnCount = 0;
string sigSceneParticles = "none";
_outdoorSceneParticleEntityIds.Clear();
_visibleSceneParticleEntityIds.Clear();
// Retail entry ownership: GameWindow never builds a second indoor PView product.
// Outdoor frames begin no-clip; indoor frames skip the global landscape block and let
@ -3591,10 +3622,10 @@ public sealed class GameWindow : IDisposable
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup.
EnableClipDistances();
_worldRenderDiagnostics?.BeginTerrainDraw();
_terrainDrawDiagnostics!.Begin();
sigTerrainDrawn = true;
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
PublishTerrainDrawDiagnostics();
_terrainDrawDiagnostics.Complete();
}
@ -3617,7 +3648,7 @@ public sealed class GameWindow : IDisposable
if (_retailPViewRenderer is null)
throw new InvalidOperationException("Retail PView renderer is required for indoor frames.");
var pviewResult = _retailPViewRenderer.DrawInside(new AcDream.App.Rendering.RetailPViewDrawContext
var pviewResult = _retailPViewRenderer.DrawInside(new AcDream.App.Rendering.RetailPViewFrameInput
{
RootCell = clipRoot,
// R-A2: outdoor root floods each nearby building per-building (not via the root).
@ -3626,7 +3657,7 @@ public sealed class GameWindow : IDisposable
NearbyBuildingCells = nearbyBuildingCells,
ViewerEyePos = viewerEyePos,
ViewProjection = envCellViewProj,
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
Cells = _retailPViewCells!,
Camera = camera,
CameraWorldPosition = camPos,
// (#176 correction: the former RebuildScopedLights callback —
@ -3640,94 +3671,20 @@ public sealed class GameWindow : IDisposable
RenderCenterLbY = renderCenterLbY,
RenderRadius = _nearRadius,
LandblockEntries = _worldState.LandblockEntries,
SetTerrainClipUbo = binding => _terrain?.SetClipUbo(binding),
DrawLandscapeSlice = sliceCtx =>
DrawRetailPViewLandscapeSlice(
sliceCtx,
camera,
frustum,
camPos,
playerLb,
animatedIds,
renderSky,
renderWeather: playerSeenOutside,
kf,
environOverrideActive),
// #131/#132: the late phase — dynamics meshes + scene
// particles + weather AFTER the look-ins (FlushAlphaList
// deferral).
DrawLandscapeSliceLate = lateCtx =>
DrawRetailPViewLandscapeSliceLate(
lateCtx,
camera,
frustum,
camPos,
playerLb,
animatedIds,
renderSky,
renderWeather: playerSeenOutside,
kf,
environOverrideActive,
isOutdoorRoot: clipRoot.IsOutdoorNode),
// T1: retail's depth discipline (PView::DrawCells, Ghidra 0x005a4840).
// INTERIOR roots: one FULL depth clear between the outside stage and
// the interior stage, then SEALS re-stamp every outside-leading
// portal's TRUE depth (#108's protective mechanism). OUTDOOR roots:
// no clear (the world's depth must survive) — instead each flooded
// building's entry aperture gets a far-Z PUNCH so its interior shows
// through the doorway. Both are safe ONLY because dynamics draw LAST
// (DrawDynamicsLast) — the first BR-2 attempt punched after dynamics
// and erased the player (reverted 88be519).
ClearDepthForInterior = clipRoot.IsOutdoorNode
? null
: () =>
{
gl.Disable(EnableCap.ScissorTest);
gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson)
gl.Clear(ClearBufferMask.DepthBufferBit);
},
DrawExitPortalMasks = sliceCtx =>
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
forceFarZ: clipRoot.IsOutdoorNode),
// #124: look-in apertures are ALWAYS the punch (retail
// maxZ1), independent of the root-keyed selector above.
DrawLookInPortalPunch = sliceCtx =>
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
forceFarZ: true),
// #131: unattached emitters under an interior root — the
// landscape-stage pass (the outdoor T3 pass below is gated
// IsOutdoorNode, so the two never both run).
DrawUnattachedSceneParticles = () =>
{
if (_particleSystem is null || _particleRenderer is null)
return;
DisableClipDistances();
_particleRenderer.DrawForOwners(
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_noSceneParticleEntityIds,
includeUnattached: true);
},
FlushLandscapeAlpha = _retailAlphaQueue.Flush,
DrawCellParticles = sliceCtx =>
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
DrawDynamicsParticles = survivors =>
DrawRetailPViewDynamicsParticles(survivors, camera, camPos),
EmitDiagnostics = result =>
_worldRenderDiagnostics?.EmitRetailPViewDiagnostics(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeViewerEnabled,
AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled,
AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled,
result,
clipRoot,
viewerCellId,
playerCellId,
camPos,
playerViewPos,
camera.View,
_cellVisibility.LastCameraCellResolution),
});
RenderSky = renderSky,
RenderWeather = playerSeenOutside,
DayFraction = (float)WorldTime.DayFraction,
ActiveDayGroup = _activeDayGroup,
SkyKeyframe = kf,
EnvironOverrideActive = environOverrideActive,
ViewerCellId = viewerCellId,
PlayerCellId = playerCellId,
PlayerViewPosition = playerViewPos,
CameraView = camera.View,
CameraCellResolution = _cellVisibility.LastCameraCellResolution,
}, _retailPViewPassExecutor
?? throw new InvalidOperationException(
"Retail PView pass executor is required for indoor frames."));
pvFrame = pviewResult.PortalFrame;
clipAssembly = pviewResult.ClipAssembly;
envCellShellFilter = pviewResult.DrawableCells;
@ -3833,7 +3790,7 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_visibleSceneParticleEntityIds,
includeUnattached: true,
excludedAttachedOwnerIds: _outdoorSceneParticleEntityIds);
excludedAttachedOwnerIds: _noSceneParticleEntityIds);
}
else
{
@ -3865,13 +3822,13 @@ public sealed class GameWindow : IDisposable
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_outdoorSceneParticleEntityIds,
_retailPViewPassExecutor!.OutdoorSceneParticleEntityIds,
includeUnattached: true);
}
// Bug A fix (post-#26 worktree, 2026-04-26): weather sky
// Outdoor LScape post-scene weather. Indoor weather through an exit portal is
// drawn by RetailPViewRenderer.DrawInside via DrawRetailPViewLandscapeSlice.
// drawn by RetailPViewRenderer.DrawInside via RetailPViewPassExecutor.
if (clipRoot is null && drawSkyThisFrame)
{
sigSkyDrawn = true;
@ -4257,308 +4214,6 @@ public sealed class GameWindow : IDisposable
}
}
private void DrawRetailPViewLandscapeSlice(
AcDream.App.Rendering.RetailPViewLandscapeSliceContext sliceCtx,
ICamera camera,
FrustumPlanes? frustum,
System.Numerics.Vector3 camPos,
uint? playerLb,
HashSet<uint>? animatedIds,
bool renderSky,
bool renderWeather,
AcDream.Core.World.SkyKeyframe kf,
bool environOverrideActive)
{
var slice = sliceCtx.Slice;
bool scissor = BeginDoorwayScissor(true, slice.NdcAabb);
_worldRenderDiagnostics?.EmitClipRouteScissorProbe(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled,
scissor,
slice.NdcAabb);
_clipFrame!.BindTerrainClip(_gl!);
EnableClipDistances();
if (renderSky)
_skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction,
_activeDayGroup, kf, environOverrideActive);
DisableClipDistances();
if (renderSky && _particleSystem is not null && _particleRenderer is not null)
_particleRenderer.Draw(camera, camPos,
AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene);
EnableClipDistances();
_worldRenderDiagnostics?.BeginTerrainDraw();
_terrain?.Draw(
camera,
frustum,
neverCullLandblockId: playerLb,
clipPlanes: slice.Planes,
ndcClipAabb: slice.NdcAabb);
PublishTerrainDrawDiagnostics();
// T3 (BR-5): entities draw OUTSIDE the clip bracket — retail meshes
// are viewcone-CHECKED, never hard-clipped (Ghidra 0x0054c250); the
// sphere pre-filter already ran in RetailPViewRenderer (OutdoorEntities
// is the per-slice survivor set).
DisableClipDistances();
if (sliceCtx.OutdoorEntities.Count > 0)
{
var sceneryEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero,
sliceCtx.OutdoorEntities,
(IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)null);
_wbDrawDispatcher!.Draw(camera, new[] { sceneryEntry }, frustum,
neverCullLandblockId: playerLb,
visibleCellIds: null,
animatedEntityIds: animatedIds);
}
// #131/#132: scene particles + weather MOVED to the LATE phase
// (DrawRetailPViewLandscapeSliceLate) — they must composite AFTER the
// #124 look-ins (retail's FlushAlphaList deferral, DrawCells
// pc:432722); drawn here they were overpainted by far-building
// interiors wherever a look-in aperture sat behind them.
if (scissor)
_gl!.Disable(EnableCap.ScissorTest);
DisableClipDistances();
}
// #131/#132: the LATE landscape phase — per slice, invoked by the renderer
// AFTER the #124 look-in sub-pass, still pre-clear. Outside-stage
// dynamics' meshes (a translucent portal swirl blends over a far interior
// instead of being overpainted by it — translucents write no depth to
// protect themselves) + ALL attached scene particles (statics' flames
// included — the #132 candle) + weather. Retail equivalent: alpha draws
// collected during LScape::draw flush ONCE after it
// (D3DPolyRender::FlushAlphaList, PView::DrawCells pc:432722).
private void DrawRetailPViewLandscapeSliceLate(
AcDream.App.Rendering.RetailPViewLandscapeLateSliceContext lateCtx,
ICamera camera,
FrustumPlanes? frustum,
System.Numerics.Vector3 camPos,
uint? playerLb,
HashSet<uint>? animatedIds,
bool renderSky,
bool renderWeather,
AcDream.Core.World.SkyKeyframe kf,
bool environOverrideActive,
bool isOutdoorRoot)
{
var slice = lateCtx.Slice;
bool scissor = BeginDoorwayScissor(true, slice.NdcAabb);
_clipFrame!.BindTerrainClip(_gl!);
// Outside-stage dynamics' meshes — viewcone pre-filtered by the
// renderer, never hard-clipped (T3).
DisableClipDistances();
if (lateCtx.Dynamics.Count > 0)
{
var dynamicsEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero,
lateCtx.Dynamics,
(IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)null);
_wbDrawDispatcher!.Draw(camera, new[] { dynamicsEntry }, frustum,
neverCullLandblockId: playerLb,
visibleCellIds: null,
animatedEntityIds: animatedIds);
}
_outdoorSceneParticleEntityIds.Clear();
foreach (var entity in lateCtx.ParticleOwners)
_outdoorSceneParticleEntityIds.Add(ParticleEntityKey(entity));
// #131 [outstage-pt] probe: the slice Scene-particle id set + how many
// live emitters the filter would actually match, plus the distinct
// UNMATCHED attached owner ids (the portal-identification handle —
// an emitter whose owner never lands in the set draws nowhere
// indoors). Print-on-change.
_worldRenderDiagnostics?.EmitOutStageParticles(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled,
_particleSystem,
_outdoorSceneParticleEntityIds);
// #132 outdoor sibling: under an OUTDOOR root the merged building
// interiors draw AFTER this stage (DrawEnvCellShells) — a flame drawn
// here is overpainted whenever a punched aperture sits behind it
// (user-confirmed at the outdoor candle). Outdoor roots therefore
// SKIP the slice Scene pass and draw attached scene particles in the
// post-frame pass alongside the T3 unattached pass (the id set built
// above carries over — the outdoor root has a single full-screen
// slice). Interior roots draw here: the look-ins already ran and the
// post-clear seal discipline owns the rest of the frame.
if (!isOutdoorRoot
&& _outdoorSceneParticleEntityIds.Count > 0
&& _particleSystem is not null
&& _particleRenderer is not null)
{
_particleRenderer.DrawForOwners(
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_outdoorSceneParticleEntityIds);
}
// T3 (BR-5): weather gates on the PLAYER being outside, not the viewer
// root — retail draws weather only when is_player_outside (the rain
// cylinder rides the player; an indoor player gets NO rain even while
// looking out a doorway). Closes the rain-through-doorways divergence
// (weather-gate-player-vs-viewer, adjusted-confirmed).
EnableClipDistances();
if (renderSky && renderWeather)
{
_skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction,
_activeDayGroup, kf, environOverrideActive);
DisableClipDistances();
if (_particleSystem is not null && _particleRenderer is not null)
_particleRenderer.Draw(camera, camPos,
AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene);
}
else
{
DisableClipDistances();
}
if (scissor)
_gl!.Disable(EnableCap.ScissorTest);
DisableClipDistances();
}
// T1: retail's invisible portal depth writes on every outside-leading
// portal (other_cell_id==0xFFFF) of this cell, clipped to the slice's view
// region — D3DPolyRender::DrawPortalPolyInternal (Ghidra 0x0059bc90),
// dispatched by PView::DrawCells (Ghidra 0x005a4840). forceFarZ is
// retail's maxZ1(true)/maxZ2(false) selector:
// • INTERIOR root (false → SEAL, true depth): after the full depth clear,
// stamp the door plane so interior geometry beyond the door z-fails
// inside the aperture and the terrain drawn through the outside view
// keeps its pixels (#108's protective mechanism).
// • OUTDOOR root / look-in (true → PUNCH, far depth): erase the world's
// depth inside a flooded building's entry aperture so the interior
// drawn next shows THROUGH the doorway.
// Both are safe ONLY because dynamics draw last (DrawDynamicsLast) — the
// first BR-2 attempt punched after dynamics and erased the player
// (reverted 88be519). Wiring only — the draw lives in
// PortalDepthMaskRenderer.
private void DrawRetailPViewPortalDepthWrite(
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
System.Numerics.Matrix4x4 viewProjection,
bool forceFarZ)
{
if (_portalDepthMask is null)
return;
if (!_cellVisibility.TryGetCell(sliceCtx.CellId, out var cell) || cell is null)
return;
Span<System.Numerics.Vector3> world = stackalloc System.Numerics.Vector3[32];
for (int i = 0; i < cell.Portals.Count; i++)
{
if (cell.Portals[i].OtherCellId != 0xFFFF)
continue; // depth writes apply to portals leading OUTSIDE only
if (i >= cell.PortalPolygons.Count)
break;
var localVerts = cell.PortalPolygons[i];
if (localVerts.Length < 3)
continue;
// cell.WorldTransform is the PHYSICS (unlifted) transform (f35cb8b);
// the shell that rasterizes this aperture draws +ShellDrawLiftZ
// higher. The seal/punch is a DRAW — stamp depth in the same lifted
// space or the stamp sits 2 cm below the drawn hole (#130 family).
int n = System.Math.Min(localVerts.Length, world.Length);
for (int v = 0; v < n; v++)
{
world[v] = System.Numerics.Vector3.Transform(localVerts[v], cell.WorldTransform);
world[v].Z += AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ;
}
// #176 seam-draw probe: a sealed dungeon must emit ZERO depth fans
// (only OtherCellId==0xFFFF portals reach here) — any line in a
// target cell is a finding (a depth stamp fighting the shell floor).
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(sliceCtx.CellId))
{
float seamZMin = float.MaxValue, seamZMax = float.MinValue;
for (int v = 0; v < n; v++)
{
seamZMin = System.Math.Min(seamZMin, world[v].Z);
seamZMax = System.Math.Max(seamZMax, world[v].Z);
}
Console.WriteLine(System.FormattableString.Invariant(
$"[seam-mask] t={System.Environment.TickCount64} cell=0x{sliceCtx.CellId:X8} portal={i} far={forceFarZ} n={n} z=[{seamZMin:F3},{seamZMax:F3}]"));
}
_portalDepthMask.DrawDepthFan(world[..n], viewProjection, sliceCtx.Slice.Planes, forceFarZ);
}
}
private void DrawRetailPViewCellParticles(
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
ICamera camera,
System.Numerics.Vector3 camPos)
{
if (_particleSystem is null || _particleRenderer is null || sliceCtx.CellEntities.Count == 0)
return;
_visibleSceneParticleEntityIds.Clear();
foreach (var entity in sliceCtx.CellEntities)
_visibleSceneParticleEntityIds.Add(ParticleEntityKey(entity));
if (_visibleSceneParticleEntityIds.Count == 0)
return;
// T3 (BR-5): the scissor-AABB gate is DELETED — retail gates particles
// like meshes (viewcone on the owner; depth does the pixels). The
// CellEntities set is already the cone-surviving owner list, so the
// id-predicate below IS the cone gate; the punch/seal depth discipline
// composites the pixels.
DisableClipDistances();
_particleRenderer.DrawForOwners(
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_visibleSceneParticleEntityIds);
DisableClipDistances();
}
// #121: the dynamics-owner particle pass — Scene-pass emitters attached to
// the frame's cone-surviving dynamics (portal swirls on server-spawned
// portal entities, creature effects). Retail draws emitters with their
// owner object; before this pass, dynamics' emitters fell through every
// pview particle filter (landscape slice = outdoor statics + #118
// outside-stage dynamics; cell callback = cell statics) once T4 deleted
// the clipRoot==null global pass from normal frames — every world portal
// went invisible. Mirror of DrawRetailPViewCellParticles with the
// survivors' ids as the filter.
private readonly HashSet<uint> _dynamicsSceneParticleEntityIds = new();
private void DrawRetailPViewDynamicsParticles(
IReadOnlyList<AcDream.Core.World.WorldEntity> survivors,
ICamera camera,
System.Numerics.Vector3 camPos)
{
if (_particleSystem is null || _particleRenderer is null || survivors.Count == 0)
return;
_dynamicsSceneParticleEntityIds.Clear();
foreach (var entity in survivors)
_dynamicsSceneParticleEntityIds.Add(ParticleEntityKey(entity));
if (_dynamicsSceneParticleEntityIds.Count == 0)
return;
DisableClipDistances();
_particleRenderer.DrawForOwners(
camera,
camPos,
AcDream.Core.Vfx.ParticleRenderPass.Scene,
_dynamicsSceneParticleEntityIds);
DisableClipDistances();
}
private void EnableClipDistances()
{
for (int i = 0; i < ClipFrame.MaxPlanes; i++)
@ -4571,23 +4226,6 @@ public sealed class GameWindow : IDisposable
_gl!.Disable(EnableCap.ClipDistance0 + i);
}
// Phase W Stage 4: set a glScissor to an NDC AABB (the doorway / OutsideView region) in
// framebuffer pixels and enable the scissor test; returns true iff applied (the caller then
// disables EnableCap.ScissorTest after its draw/clear). Used to bracket the landscape slice
// (sky, terrain, statics, weather — particle.vert has no gl_ClipDistance). Returns false
// (no scissor) when not applied (outdoor / no window). The box is the CONSERVATIVE outer
// bound (NdcScissorRect): the previous Floor(origin)+Ceiling(size) form cut up to one pixel
// off the TOP/RIGHT edges at unlucky alignments — the #130 doorway top-edge background strip.
private bool BeginDoorwayScissor(bool apply, System.Numerics.Vector4 ndcAabb)
{
if (!apply || _window is null) return false;
var fb = _window.FramebufferSize;
var box = NdcScissorRect.ToPixels(ndcAabb, fb.X, fb.Y);
_gl!.Enable(EnableCap.ScissorTest);
_gl.Scissor(box.X, box.Y, (uint)box.Width, (uint)box.Height);
return true;
}
// ── Phase I.2 — DebugPanel helpers ────────────────────────────────
//
// The ImGui DebugPanel reads through DebugVM closures that ask
@ -5414,60 +5052,6 @@ public sealed class GameWindow : IDisposable
}
}
private void PublishTerrainDrawDiagnostics()
{
long now = _frameDiag ? Environment.TickCount64 : 0L;
_worldRenderDiagnostics?.EndTerrainDraw();
if (!_frameDiag || now - _frameDiagLastPublicationMilliseconds <= 5000)
return;
// Preserve the original one-timestamp diagnostic transaction. A
// failure in either write leaves the shared cadence uncommitted, so
// the next terrain draw retries both TERRAIN and FRAME diagnostics.
_worldRenderDiagnostics?.PublishTerrainDiagnostics(
new AcDream.App.Rendering.TerrainRenderDiagnosticFacts(
_terrain?.VisibleSlots ?? 0,
_terrain?.LoadedSlots ?? 0,
_terrain?.CapacitySlots ?? 0));
PublishFrameDiagnostics();
_frameDiagLastPublicationMilliseconds = now;
}
private void PublishFrameDiagnostics()
{
AcDream.App.Streaming.LandblockPresentationDiagnostics presentation =
_landblockPresentationPipeline?.Diagnostics ?? default;
AcDream.App.Streaming.LandblockRenderPublisherDiagnostics render =
presentation.Render;
AcDream.App.Streaming.LandblockPhysicsPublisherDiagnostics physics =
presentation.Physics;
AcDream.App.Streaming.LandblockStaticPresentationDiagnostics statics =
presentation.Statics;
double ticksToMicros =
1_000_000.0 / System.Diagnostics.Stopwatch.Frequency;
AcDream.App.Rendering.RollingTimingPercentiles upload =
_renderFrameLivePreparation?.UploadTiming ?? default;
double uploadMedian = upload.MedianHundredthsMicroseconds / 100.0;
double uploadP95 = upload.Percentile95HundredthsMicroseconds / 100.0;
int walked = _wbDrawDispatcher?.LastDrawStats.EntitiesWalked ?? 0;
_renderDiagnosticLog.WriteLine(
$"[FRAME-DIAG] publish={render.BeginCount}/{render.CompleteCount} " +
$"render_total_us=[terrain={render.TerrainPublishTicks * ticksToMicros:F1} " +
$"begin={render.BeginPublishTicks * ticksToMicros:F1} " +
$"complete={render.CompletePublishTicks * ticksToMicros:F1}] " +
$"physics_total_us=[base={physics.BasePublishTicks * ticksToMicros:F1} " +
$"gfx={physics.GfxCacheTicks * ticksToMicros:F1} " +
$"complete={physics.CompletePublishTicks * ticksToMicros:F1}] " +
$"static={statics.BeginCount}/{statics.CompleteCount}" +
$"(active={statics.ActiveEntityCount}) " +
$"entUpl_us={uploadMedian:F1}m/{uploadP95:F1}p95 " +
$"deferred={_streamingController?.DeferredApplyBacklog ?? 0} " +
$"fullRetire={_streamingController?.FullWindowRetirementCount ?? 0}" +
$"(landblocks={_streamingController?.LastFullWindowRetirementLandblockCount ?? 0}) " +
$"esg={_entitiesByServerGuid.Count} spawn={LastSpawns.Count} " +
$"resident={_worldState.Entities.Count} walked={walked}");
}
private void OnClosing()
=> CompleteShutdown();
@ -5521,6 +5105,10 @@ public sealed class GameWindow : IDisposable
{
_worldRenderFrameBuilder = null;
_skyPesFrame = null;
_retailPViewPassExecutor = null;
_retailPViewCells = null;
_terrainDrawDiagnostics = null;
_retailPViewRenderer = null;
}),
]),
new ResourceShutdownStage("session dependents",