feat(render): Phase A8 — wire stencil pipeline into render frame
Replaces the pre-A8 "terrain-always + depth-clear-when-inside" pattern
with WB's stencil-aware ordering when cameraInsideCell:
1. Upload portal triangle mesh from VisibleCellIds → LoadedCell.
2. Draw indoor entities (EntitySet.IndoorOnly) — stencil OFF.
3. Mark portal stencil + punch far depth (MarkAndPunch).
4. Draw terrain — stencil-gated to portal silhouettes.
5. Draw outdoor entities (EntitySet.OutdoorOnly) — stencil-gated.
6. DisableStencil before particles/weather/UI.
Outdoor path unchanged (EntitySet.All, no stencil work).
Adds CellVisibility.TryGetCell(uint) for the VisibleCellIds → LoadedCell
materialization. Removes the now-redundant DepthBufferBit Clear that
was the old approximation.
Retail oracle: PView::DrawCells at
acclient_2013_pseudo_c.txt:432709 ("outside_view.view_count > 0" gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dcf69a1feb
commit
41c2e67cd8
2 changed files with 114 additions and 24 deletions
|
|
@ -344,6 +344,17 @@ public sealed class CellVisibility
|
||||||
local.Z <= cell.LocalBoundsMax.Z + PointInCellEpsilon;
|
local.Z <= cell.LocalBoundsMax.Z + PointInCellEpsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a loaded cell by full 32-bit cell id, or returns null if
|
||||||
|
/// not loaded. Used by the Phase A8 stencil pipeline to materialize
|
||||||
|
/// <see cref="VisibilityResult.VisibleCellIds"/> back into
|
||||||
|
/// <see cref="LoadedCell"/> instances for portal mesh extraction.
|
||||||
|
/// </summary>
|
||||||
|
public LoadedCell? TryGetCell(uint cellId)
|
||||||
|
{
|
||||||
|
return _cellLookup.TryGetValue(cellId, out var cell) ? cell : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Brute-force scan of every loaded cell to test whether
|
/// Brute-force scan of every loaded cell to test whether
|
||||||
/// <paramref name="worldPoint"/> is inside any of them. Does not touch
|
/// <paramref name="worldPoint"/> is inside any of them. Does not touch
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ public sealed class GameWindow : IDisposable
|
||||||
// Step 4: portal-based interior cell visibility.
|
// Step 4: portal-based interior cell visibility.
|
||||||
private readonly CellVisibility _cellVisibility = new();
|
private readonly CellVisibility _cellVisibility = new();
|
||||||
|
|
||||||
|
// Phase A8: indoor-cell stencil pipeline (#78). Null until OnLoad runs
|
||||||
|
// (requires GL context). Never null after OnLoad completes normally.
|
||||||
|
private IndoorCellStencilPipeline? _indoorStencil;
|
||||||
|
|
||||||
// Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe.
|
// Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe.
|
||||||
// DatReaderWriter's DatBinReader uses a shared buffer position internally —
|
// DatReaderWriter's DatBinReader uses a shared buffer position internally —
|
||||||
// concurrent _dats.Get<T> calls from the streaming worker thread (T11+) and
|
// concurrent _dats.Get<T> calls from the streaming worker thread (T11+) and
|
||||||
|
|
@ -1769,6 +1773,13 @@ public sealed class GameWindow : IDisposable
|
||||||
// the player.
|
// the player.
|
||||||
_particleRenderer = new ParticleRenderer(_gl, shadersDir, _textureCache, _dats);
|
_particleRenderer = new ParticleRenderer(_gl, shadersDir, _textureCache, _dats);
|
||||||
|
|
||||||
|
// Phase A8 — indoor-cell visibility culling pipeline (#78).
|
||||||
|
// Shader files are deployed alongside the existing terrain/mesh
|
||||||
|
// shaders via the same .csproj content glob.
|
||||||
|
_indoorStencil = new IndoorCellStencilPipeline(_gl,
|
||||||
|
Path.Combine(shadersDir, "portal_stencil.vert"),
|
||||||
|
Path.Combine(shadersDir, "portal_stencil.frag"));
|
||||||
|
|
||||||
// A.5 T22.5: apply radii from the already-resolved _resolvedQuality.
|
// A.5 T22.5: apply radii from the already-resolved _resolvedQuality.
|
||||||
// _resolvedQuality was set by the quality block immediately after
|
// _resolvedQuality was set by the quality block immediately after
|
||||||
// LoadAndApplyPersistedSettings() above, absorbing all env-var overrides.
|
// LoadAndApplyPersistedSettings() above, absorbing all env-var overrides.
|
||||||
|
|
@ -7097,26 +7108,38 @@ public sealed class GameWindow : IDisposable
|
||||||
goto SkipWorldGeometry;
|
goto SkipWorldGeometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup
|
// Phase A8 — indoor-cell visibility culling.
|
||||||
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
|
// When the camera is inside an EnvCell, build a portal-silhouette
|
||||||
// is cheap; only the periodic Console.WriteLine is gated.
|
// stencil mask and use it to gate outdoor passes (terrain +
|
||||||
_terrainCpuStopwatch.Restart();
|
// outdoor entities) so they only write fragments inside actual
|
||||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
// portal openings. WB-style stencil port (closes #78 + cellar-
|
||||||
_terrainCpuStopwatch.Stop();
|
// stairs artifact). Retail oracle: PView::DrawCells at
|
||||||
// Multiply by 100 then divide by 100 in the diag print to keep
|
// acclient_2013_pseudo_c.txt:432709 ("outside_view.view_count > 0").
|
||||||
// 0.01 µs precision in the long-typed sample buffer. Terrain Draw
|
int portalTriCount = 0;
|
||||||
// is sub-microsecond on simple scenes; truncating to integer µs
|
if (cameraInsideCell && visibility is not null && _indoorStencil is not null)
|
||||||
// would round nearly every sample to 0.
|
{
|
||||||
_terrainCpuSamples[_terrainCpuSampleCursor] = (long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds * 100.0);
|
// Resolve VisibleCellIds → LoadedCell list via
|
||||||
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
|
// CellVisibility.TryGetCell (added in Task 7).
|
||||||
MaybeFlushTerrainDiag();
|
var currentBuildingCells = new List<LoadedCell>(visibility.VisibleCellIds.Count);
|
||||||
|
foreach (var id in visibility.VisibleCellIds)
|
||||||
|
{
|
||||||
|
var cell = _cellVisibility.TryGetCell(id);
|
||||||
|
if (cell is not null) currentBuildingCells.Add(cell);
|
||||||
|
}
|
||||||
|
portalTriCount = _indoorStencil.UploadPortalMesh(currentBuildingCells);
|
||||||
|
|
||||||
// Conditional depth clear: when camera is inside a building, clear
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||||
// depth (not color) so interior geometry writes fresh Z values on top
|
{
|
||||||
// of the terrain color buffer. Exit portals show outdoor terrain color
|
Console.WriteLine(
|
||||||
// because we kept the color buffer. Matching ACME GameScene.cs pattern.
|
$"[vis] cameraInside=true cells={visibility.VisibleCellIds.Count} " +
|
||||||
if (cameraInsideCell)
|
$"exitPortalVisible={visibility.HasExitPortalVisible} " +
|
||||||
_gl!.Clear(ClearBufferMask.DepthBufferBit);
|
$"portalTris={portalTriCount / 3}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[vis] cameraInside=false");
|
||||||
|
}
|
||||||
|
|
||||||
// L-fix1 (2026-04-28): pass the set of animated-entity ids so
|
// L-fix1 (2026-04-28): pass the set of animated-entity ids so
|
||||||
// the renderer keeps remote players / NPCs / monsters
|
// the renderer keeps remote players / NPCs / monsters
|
||||||
|
|
@ -7134,11 +7157,65 @@ public sealed class GameWindow : IDisposable
|
||||||
animatedIds.Add(k);
|
animatedIds.Add(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
// N.5: WbDrawDispatcher is always non-null (modern path mandatory).
|
if (cameraInsideCell && portalTriCount > 0)
|
||||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
{
|
||||||
neverCullLandblockId: playerLb,
|
// WB Step 3: draw indoor entities first, stencil OFF.
|
||||||
visibleCellIds: visibility?.VisibleCellIds,
|
// Indoor entities (cell mesh + cell statics) ALWAYS draw
|
||||||
animatedEntityIds: animatedIds);
|
// inside the camera-building, regardless of portal coverage.
|
||||||
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
|
neverCullLandblockId: playerLb,
|
||||||
|
visibleCellIds: visibility!.VisibleCellIds,
|
||||||
|
animatedEntityIds: animatedIds,
|
||||||
|
set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.IndoorOnly);
|
||||||
|
|
||||||
|
// WB Steps 1+2: mark stencil + punch far depth in portal regions.
|
||||||
|
_indoorStencil!.MarkAndPunch(camera.View * camera.Projection);
|
||||||
|
|
||||||
|
// WB Step 4a: terrain, stencil-gated to portal silhouettes.
|
||||||
|
_indoorStencil!.EnableOutdoorPass();
|
||||||
|
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup
|
||||||
|
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
|
||||||
|
// is cheap; only the periodic Console.WriteLine is gated.
|
||||||
|
_terrainCpuStopwatch.Restart();
|
||||||
|
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||||
|
_terrainCpuStopwatch.Stop();
|
||||||
|
_terrainCpuSamples[_terrainCpuSampleCursor] =
|
||||||
|
(long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds * 100.0);
|
||||||
|
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
|
||||||
|
MaybeFlushTerrainDiag();
|
||||||
|
|
||||||
|
// WB Step 4b: outdoor entities, still stencil-gated.
|
||||||
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
|
neverCullLandblockId: playerLb,
|
||||||
|
visibleCellIds: visibility.VisibleCellIds,
|
||||||
|
animatedEntityIds: animatedIds,
|
||||||
|
set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorOnly);
|
||||||
|
|
||||||
|
_indoorStencil!.DisableStencil();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Outdoor (or indoor with no exit portals): pre-A8 path.
|
||||||
|
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup.
|
||||||
|
_terrainCpuStopwatch.Restart();
|
||||||
|
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||||
|
_terrainCpuStopwatch.Stop();
|
||||||
|
// Multiply by 100 then divide by 100 in the diag print to keep
|
||||||
|
// 0.01 µs precision in the long-typed sample buffer. Terrain Draw
|
||||||
|
// is sub-microsecond on simple scenes; truncating to integer µs
|
||||||
|
// would round nearly every sample to 0.
|
||||||
|
_terrainCpuSamples[_terrainCpuSampleCursor] =
|
||||||
|
(long)(_terrainCpuStopwatch.Elapsed.TotalMicroseconds * 100.0);
|
||||||
|
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
|
||||||
|
MaybeFlushTerrainDiag();
|
||||||
|
|
||||||
|
// N.5: WbDrawDispatcher is always non-null (modern path mandatory).
|
||||||
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
|
neverCullLandblockId: playerLb,
|
||||||
|
visibleCellIds: visibility?.VisibleCellIds,
|
||||||
|
animatedEntityIds: animatedIds,
|
||||||
|
set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.All);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase G.1 / E.3: draw all live particles after opaque
|
// Phase G.1 / E.3: draw all live particles after opaque
|
||||||
// scene geometry so alpha blending composites correctly.
|
// scene geometry so alpha blending composites correctly.
|
||||||
|
|
@ -10494,6 +10571,8 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveSession = null;
|
_liveSession = null;
|
||||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||||
_wbDrawDispatcher?.Dispose();
|
_wbDrawDispatcher?.Dispose();
|
||||||
|
_indoorStencil?.Dispose();
|
||||||
|
_indoorStencil = null;
|
||||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||||
_samplerCache?.Dispose();
|
_samplerCache?.Dispose();
|
||||||
_textureCache?.Dispose();
|
_textureCache?.Dispose();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue