refactor(render): extract typed retail pview passes
This commit is contained in:
parent
6d6e5b5fa5
commit
85239fb373
13 changed files with 1888 additions and 841 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue