feat(render): Phase U.4 — unified gated draw pass (indoor root)
Wire the portal-visibility result through the clip pipeline: build a per-frame ClipFrame (slot 0 no-clip, slot 1 OutsideView, slot 2..N per visible cell) + cellIdToSlot from PortalVisibilityBuilder; call the (previously dormant) EnvCellRenderer.Render for cell shells inside the clip bracket; assign per-instance clip slots in WbDrawDispatcher (live-dynamic unclipped per retail, cell statics to their cell slot, outdoor scenery to OutsideView, non-visible culled); gate/scissor/ skip terrain per OutsideView (empty ⇒ no terrain — the bleed fix). Emit ACDREAM_PROBE_VIS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
864fc5f94e
commit
7993e064a0
6 changed files with 748 additions and 67 deletions
|
|
@ -7130,43 +7130,12 @@ public sealed class GameWindow : IDisposable
|
|||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||
|
||||
// SPIKE 2026-05-26: A8 transition investigation. Lights up the
|
||||
// dormant RenderingDiagnostics.ProbeVisibilityEnabled flag (added
|
||||
// by Task 6 of the original A8 plan). Per-frame state captures:
|
||||
// camera position, lenient + strict inside flags side-by-side,
|
||||
// CameraCell id, VisibleCellIds list. Branch markers inside
|
||||
// indoor + outdoor branches complete the trace.
|
||||
// Enable via ACDREAM_PROBE_VIS=1.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||
{
|
||||
bool reallyInsideProbe = visibility?.CameraCell is not null
|
||||
&& CellVisibility.PointInCell(camPos, visibility.CameraCell);
|
||||
int visCount = visibility?.VisibleCellIds?.Count ?? 0;
|
||||
string visList;
|
||||
if (visibility?.VisibleCellIds is null || visCount == 0)
|
||||
{
|
||||
visList = "[]";
|
||||
}
|
||||
else
|
||||
{
|
||||
var sb = new System.Text.StringBuilder("[");
|
||||
int shown = 0;
|
||||
foreach (var id in visibility.VisibleCellIds)
|
||||
{
|
||||
if (shown >= 8) { sb.Append(",..."); break; }
|
||||
if (shown > 0) sb.Append(',');
|
||||
sb.Append($"0x{id:X8}");
|
||||
shown++;
|
||||
}
|
||||
sb.Append(']');
|
||||
visList = sb.ToString();
|
||||
}
|
||||
string cellId = visibility?.CameraCell?.CellId.ToString("X8") ?? "null";
|
||||
Console.WriteLine(
|
||||
$"[vis] pos=({camPos.X:F2},{camPos.Y:F2},{camPos.Z:F2}) " +
|
||||
$"inside={cameraInsideCell} really={reallyInsideProbe} " +
|
||||
$"cell=0x{cellId} visN={visCount} {visList}");
|
||||
}
|
||||
// Phase U.4 (2026-05-30): the [vis] probe moved DOWN to the unified
|
||||
// gated-draw block (after envCellViewProj exists) where it can report
|
||||
// the real PortalVisibilityFrame — OutsideView polygon/plane counts and
|
||||
// per-cell slot plane counts — via RenderingDiagnostics.EmitVis, instead
|
||||
// of the old camera-state-only spike. See the U.4 ClipFrame assembly
|
||||
// below (gated on ACDREAM_PROBE_VIS=1, cell-change-throttled).
|
||||
|
||||
// Lighting decisions (sun zeroed, indoor ambient applied) must
|
||||
// track the PLAYER's cell, not the camera's. In third-person
|
||||
|
|
@ -7281,16 +7250,70 @@ public sealed class GameWindow : IDisposable
|
|||
goto SkipWorldGeometry;
|
||||
}
|
||||
|
||||
// Phase U.3: build + upload the SHARED per-frame clip data once,
|
||||
// ahead of both terrain and entity draws. In U.3 this is the no-clip
|
||||
// frame (slot 0 only, terrain count 0) so the whole scene renders
|
||||
// ungated — bit-identical to pre-U.3. UploadShared binds binding=2
|
||||
// (mesh SSBO) + binding=2 (terrain UBO); each renderer below re-binds
|
||||
// its binding=2 defensively from the ids we hand it. The single
|
||||
// _clipFrame instance reuses its GL buffers across frames (NoClip is
|
||||
// cheap CPU-only state we copy into it). U.4 swaps NoClip() for the
|
||||
// real portal-visibility frame here.
|
||||
// Phase U.4: build the SHARED per-frame clip data from the portal-
|
||||
// visibility result, ahead of both terrain and entity draws.
|
||||
//
|
||||
// Root: a non-null CameraCell means the camera is INSIDE a cell (indoor
|
||||
// root) — run the portal-frame BFS (PortalVisibilityBuilder) and assemble
|
||||
// a real ClipFrame (slot 0 no-clip, slot 1.. per visible cell + the
|
||||
// OutsideView) + a cellId→slot map. A null CameraCell is the OUTDOOR root:
|
||||
// no pvFrame, the frame stays no-clip, every instance is slot 0 and terrain
|
||||
// draws normally — bit-identical to U.3 (outdoor→building peering is U.5).
|
||||
//
|
||||
// The single _clipFrame instance is RESET + repacked in place each frame
|
||||
// (ClipFrameAssembler.Assemble → ClipFrame.Reset) so its SSBO/UBO ids are
|
||||
// reused — no per-frame GL buffer churn. UploadShared binds binding=2
|
||||
// (mesh SSBO) + binding=2 (terrain UBO); each renderer re-binds its
|
||||
// binding=2 defensively from the ids we hand it.
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
var clipRoot = visibility?.CameraCell;
|
||||
ClipFrameAssembly? clipAssembly = null;
|
||||
var terrainClipMode = TerrainClipMode.Planes; // overwritten below for indoor root
|
||||
System.Numerics.Vector4 terrainScissorNdc = default;
|
||||
HashSet<uint>? envCellShellFilter = null; // drawable visible cells (cellIdToSlot keys)
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
clipRoot,
|
||||
camPos,
|
||||
id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||
envCellViewProj);
|
||||
|
||||
clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
||||
terrainClipMode = clipAssembly.TerrainMode;
|
||||
terrainScissorNdc = clipAssembly.TerrainScissorNdcAabb;
|
||||
|
||||
// Per-instance routing for the entity dispatcher + the cell shells.
|
||||
_wbDrawDispatcher?.SetClipRouting(
|
||||
clipAssembly.CellIdToSlot, clipAssembly.OutdoorSlot, clipAssembly.OutdoorVisible);
|
||||
_envCellRenderer?.SetClipRouting(clipAssembly.CellIdToSlot);
|
||||
|
||||
// The cell SHELLS render only for drawable visible cells (the slot
|
||||
// map's keys; IsNothingVisible cells were excluded by the assembler).
|
||||
envCellShellFilter = new HashSet<uint>(clipAssembly.CellIdToSlot.Keys);
|
||||
|
||||
// [vis] probe (ACDREAM_PROBE_VIS=1) — the real PortalVisibilityFrame
|
||||
// numbers, replacing the old camera-state-only spike. Cell-change
|
||||
// throttled inside EmitVis so launch.log stays readable under motion.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled)
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
|
||||
clipRoot.CellId,
|
||||
pvFrame.OrderedVisibleCells,
|
||||
pvFrame.OutsideView.Polygons.Count,
|
||||
clipAssembly.OutsidePlaneCount,
|
||||
clipAssembly.PerCellPlaneCounts,
|
||||
clipAssembly.ScissorFallbacks);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outdoor root: no portal frame. Keep the frame no-clip and revert the
|
||||
// renderers to U.3 behavior (every instance slot 0, nothing culled,
|
||||
// terrain ungated). Reset so a prior indoor frame's slots don't leak.
|
||||
_clipFrame.Reset();
|
||||
_wbDrawDispatcher?.ClearClipRouting();
|
||||
_envCellRenderer?.SetClipRouting(null);
|
||||
}
|
||||
|
||||
_clipFrame.UploadShared(_gl);
|
||||
_wbDrawDispatcher?.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
_envCellRenderer?.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
|
|
@ -7312,8 +7335,45 @@ public sealed class GameWindow : IDisposable
|
|||
// 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.
|
||||
//
|
||||
// Phase U.4 OutsideView gating (indoor root only; outdoor root uses
|
||||
// TerrainClipMode.Planes with a count-0 UBO = ungated, the U.3 path):
|
||||
// Skip ⇒ the camera sees no outdoors through any portal chain →
|
||||
// draw NO terrain. THIS is the bleed fix (empty OutsideView
|
||||
// ⇒ outdoor terrain stops leaking into interiors).
|
||||
// Scissor ⇒ OutsideView exceeded the convex-plane budget → glScissor
|
||||
// around ONLY the terrain draw (NDC AABB → framebuffer px),
|
||||
// UBO left ungated. Disabled again immediately after so the
|
||||
// rest of the frame is unscissored.
|
||||
// Planes ⇒ UBO carries the OutsideView planes (already set by the
|
||||
// assembler) → terrain gated per-vertex, draw normally.
|
||||
_terrainCpuStopwatch.Restart();
|
||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||
if (terrainClipMode == TerrainClipMode.Skip)
|
||||
{
|
||||
// No terrain this frame — bleed fix.
|
||||
}
|
||||
else if (terrainClipMode == TerrainClipMode.Scissor)
|
||||
{
|
||||
var fb = _window!.FramebufferSize;
|
||||
// NDC [-1,1] → window pixels. Clamp to the framebuffer so a portal
|
||||
// opening that extends past the screen edge yields a valid box.
|
||||
float nx0 = System.Math.Clamp(terrainScissorNdc.X, -1f, 1f);
|
||||
float ny0 = System.Math.Clamp(terrainScissorNdc.Y, -1f, 1f);
|
||||
float nx1 = System.Math.Clamp(terrainScissorNdc.Z, -1f, 1f);
|
||||
float ny1 = System.Math.Clamp(terrainScissorNdc.W, -1f, 1f);
|
||||
int px = (int)System.MathF.Floor((nx0 * 0.5f + 0.5f) * fb.X);
|
||||
int py = (int)System.MathF.Floor((ny0 * 0.5f + 0.5f) * fb.Y);
|
||||
int pw = (int)System.MathF.Ceiling((nx1 - nx0) * 0.5f * fb.X);
|
||||
int ph = (int)System.MathF.Ceiling((ny1 - ny0) * 0.5f * fb.Y);
|
||||
_gl.Enable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(px, py, (uint)System.Math.Max(0, pw), (uint)System.Math.Max(0, ph));
|
||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
}
|
||||
else
|
||||
{
|
||||
_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
|
||||
|
|
@ -7339,20 +7399,36 @@ public sealed class GameWindow : IDisposable
|
|||
animatedIds.Add(k);
|
||||
}
|
||||
|
||||
// Phase U.4: render the indoor cell SHELLS (walls / floors / ceilings)
|
||||
// — previously DORMANT (EnvCellRenderer.Render was never called in the
|
||||
// live loop). Inside the clip bracket so each cell's instances are gated
|
||||
// to its CellClip slot via the binding=3 map we installed above. Opaque
|
||||
// pass BEFORE the entity dispatcher (front-to-back, depth writes on);
|
||||
// Transparent pass AFTER. Filter = the drawable visible cells. Only when
|
||||
// there's an indoor root (clipAssembly != null) — outdoor frames draw no
|
||||
// shells. PrepareRenderBatches already ran earlier this frame.
|
||||
if (clipAssembly is not null && envCellShellFilter is not null)
|
||||
_envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, envCellShellFilter);
|
||||
|
||||
// Scene entity draw. N.5: WbDrawDispatcher is always non-null
|
||||
// (modern path mandatory). Default EntitySet.All — every entity
|
||||
// walked, gated only by the ParentCellId ∈ visibleCellIds filter.
|
||||
// Phase U: unified gated draw wired in U.4a
|
||||
// Phase U.4: per-instance clip slots come from SetClipRouting above
|
||||
// (indoor root) or ClearClipRouting (outdoor root → every instance slot 0).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
|
||||
// Phase U.4: cell shells transparent pass (additive / alpha-blend cell
|
||||
// surfaces, e.g. stained glass). Still inside the clip bracket.
|
||||
if (clipAssembly is not null && envCellShellFilter is not null)
|
||||
_envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, envCellShellFilter);
|
||||
|
||||
// Phase U.3: close the world-geometry clip bracket opened above. From
|
||||
// here down (particles, weather, debug lines, UI) the vertex shaders do
|
||||
// NOT write gl_ClipDistance, so the planes must be OFF to avoid the
|
||||
// undefined-behavior clip. U.4's EnvCellRenderer.Render, when added,
|
||||
// belongs ABOVE this line (it writes gl_ClipDistance like the others).
|
||||
// undefined-behavior clip.
|
||||
for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++)
|
||||
_gl.Disable(EnableCap.ClipDistance0 + _cp);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue