fix(render): Phase U.3 — scope gl_ClipDistance enable to world-geometry draws

Code review caught a portability hazard: GL_CLIP_DISTANCE0..7 was enabled globally
at init, but sky/particle/ui/debug vertex shaders don't write gl_ClipDistance —
undefined behavior that could clip them away on some drivers (benign on the dev
driver, which is why the offline check passed). Bracket the enable/disable around
only the terrain+entity (mesh_modern/terrain_modern) draws; sky/particles/UI/debug
render with clipping off. U.4's EnvCellRenderer.Render belongs inside the bracket.
Also: ClipFrame is long-lived (??= NoClip()), so Dispose now deletes its GL buffers;
fix the stale per-frame-transient comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-30 17:42:27 +02:00
parent bf2e559369
commit 864fc5f94e
2 changed files with 54 additions and 20 deletions

View file

@ -167,11 +167,12 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
// UBO). In U.3 this is rebuilt as ClipFrame.NoClip() every frame and uploaded
// once before terrain/entities draw, so the whole scene renders ungated
// (identical to pre-U.3). The buffer ids are handed to the three renderers so
// each re-binds binding=2 immediately before its own draw. U.4 replaces the
// NoClip() frame with one built from the portal-visibility result.
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
// REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip
// state is re-uploaded each frame before terrain/entities draw, so the whole
// scene renders ungated (identical to pre-U.3). The buffer ids are handed to the
// three renderers so each re-binds binding=2 immediately before its own draw.
// U.4 replaces the NoClip() frame with one built from the portal-visibility result.
private ClipFrame? _clipFrame;
/// <summary>
@ -1087,16 +1088,16 @@ public sealed class GameWindow : IDisposable
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
_gl.Enable(EnableCap.DepthTest);
// Phase U.3: enable all 8 hardware clip planes once at startup. The mesh
// and terrain vertex shaders write gl_ClipDistance[0..7] every frame;
// unused planes are set to +1.0 (pass-all) when a region's plane count is
// below 8, so leaving all 8 enabled permanently is correct and avoids
// per-draw glEnable/glDisable thrash. In U.3 every region is no-clip, so
// nothing is actually clipped — the running game renders identically. U.4
// populates real clip regions; the enables stay as-is.
// (EnableCap.ClipDistance0 == GL_CLIP_DISTANCE0 0x3000; +i selects plane i.)
for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++)
_gl.Enable(EnableCap.ClipDistance0 + _cp);
// Phase U.3: the 8 hardware clip planes (GL_CLIP_DISTANCE0..7) are NOT
// enabled here. Only the mesh_modern / terrain_modern vertex shaders write
// gl_ClipDistance[0..7]; the sky / particle / weather / UI / debug-line
// shaders write gl_Position but no gl_ClipDistance. Enabling a clip plane
// for a draw whose vertex shader doesn't write that plane's distance is
// UNDEFINED per the GL/GLSL spec (a driver may read the unwritten value as
// negative and clip the primitive away). So instead of a permanent global
// enable, OnRender brackets glEnable/glDisable(GL_CLIP_DISTANCE0..7) around
// ONLY the clip-writing world-geometry draws (terrain + entities, plus
// U.4's EnvCellRenderer.Render); everything else draws with clipping off.
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
@ -7295,6 +7296,19 @@ public sealed class GameWindow : IDisposable
_envCellRenderer?.SetClipRegionSsbo(_clipFrame.RegionSsbo);
_terrain?.SetClipUbo(_clipFrame.TerrainUbo);
// Phase U.3: enable the 8 hardware clip planes for the world-geometry
// block ONLY. All gl_ClipDistance-writing draws (terrain, entities, and
// U.4's EnvCellRenderer.Render) MUST be inside this enable/disable
// bracket; everything else (sky, particles, weather, debug, UI) renders
// with clip DISABLED. Sky already drew above (must not be clipped);
// particles/weather/debug/UI draw below the matching glDisable. Scoping
// the enable here (instead of a permanent init-time enable) avoids the
// undefined behavior of leaving GL_CLIP_DISTANCE_i on for shaders that
// never write gl_ClipDistance[i] — a driver is free to clip those away.
// (EnableCap.ClipDistance0 == GL_CLIP_DISTANCE0 0x3000; +i selects plane i.)
for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++)
_gl.Enable(EnableCap.ClipDistance0 + _cp);
// 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.
@ -7334,6 +7348,14 @@ public sealed class GameWindow : IDisposable
visibleCellIds: visibility?.VisibleCellIds,
animatedEntityIds: animatedIds);
// 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).
for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++)
_gl.Disable(EnableCap.ClipDistance0 + _cp);
// Phase G.1 / E.3: draw all live particles after opaque
// scene geometry so alpha blending composites correctly.
// Runs with depth test on (particles occluded by walls)