From b595cfbb9fc7b1596f97ce36549d7b90e1fb56d5 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 16:57:11 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20Phase=20W=20Stage=204=20?= =?UTF-8?q?=E2=80=94=20scissor=20sky/weather=20mesh=20in=20Scissor=20mode?= =?UTF-8?q?=20(adversarial-review=20fix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus adversarial review caught a real gap: the sky/weather MESH bled full-screen indoors in TerrainClipMode.Scissor (a multi-exit interior, or an OutsideView with >8 edges). The assembler only sets the binding=2 clip-plane UBO in Planes mode; in Scissor mode it leaves count==0, so sky.vert's gl_ClipDistance writes all +1 (no clip) and the mesh draws — which had NO scissor wrapper, only the no-op planes — covered the whole screen. The terrain and particle passes were already scissored; the sky/weather mesh was the one unguarded path. Fix: scissor the WHOLE sky pre-scene + weather post-scene blocks (mesh + particles) to the OutsideView AABB when indoors. In Planes mode the scissor is a harmless over-include (the per-vertex clip planes are tighter and do the exact doorway clip); in Scissor mode it is the sole confinement, mirroring the terrain Scissor path; outdoors it is skipped (full-screen, bit-identical). Also hoisted the scissor-disable out of the particle null-check (cleaner, leak-free on the no-particle path) and corrected a stale 'weather does not write gl_ClipDistance' comment at the world-bracket close. The single-convex-doorway case (Holtburg cottage) was already correct (Planes mode); this seals the multi-opening case. Build 0/0; App tests 171/171. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 54 +++++++++++++++---------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d59a9ac..8ea9f6a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7391,11 +7391,21 @@ public sealed class GameWindow : IDisposable System.Numerics.Vector4 skyDoorwayNdc = clipAssembly?.OutsideViewNdcAabb ?? default; if (drawSkyThisFrame) { - // Sky MESH: enable the 8 clip planes around RenderSky so sky.vert clips it to the - // OutsideView (binding=2 UBO: count>0 indoor → confined to the doorway; count==0 - // outdoor → all distances +1 → full-screen, bit-identical to pre-Stage-4). Re-bind - // binding=2 (UBO namespace) defensively — SkyRenderer does not own it, and we must not - // inherit whatever was last bound (memory: render-self-contained-gl-state). + // Scissor the WHOLE sky pre-scene block (mesh + particles) to the doorway AABB when + // indoors. The sky MESH is precisely clipped by sky.vert's gl_ClipDistance in PLANES + // mode (the scissor is then a harmless over-include — the planes are tighter); but in + // SCISSOR mode the OutsideView exceeded the convex-plane budget so the assembler left + // the binding=2 UBO at count 0 (no planes) — there the scissor is the ONLY confinement, + // exactly mirroring the terrain Scissor path. Without this, a multi-exit interior would + // bleed full-screen sky/rain (sky.vert with count 0 writes all +1 = no clip). The + // SkyPreScene particles (particle.vert, no gl_ClipDistance) rely on the scissor in BOTH + // modes. Outdoors (skyDoorwayClip=false) → no scissor → full-screen, bit-identical. + bool skySc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); + + // Sky MESH: re-bind binding=2 (the OutsideView UBO) defensively — SkyRenderer does not + // own it and we must not inherit whatever was last bound (memory: + // render-self-contained-gl-state) — then enable the 8 clip planes so sky.vert clips + // precisely in Planes mode (count>0). count==0 (outdoor / Scissor-mode) → all +1. _gl.BindBufferBase(BufferTargetARB.UniformBuffer, ClipFrame.TerrainClipUboBinding, _clipFrame.TerrainUbo); for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) @@ -7405,15 +7415,12 @@ public sealed class GameWindow : IDisposable for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); - // SkyPreScene particles (particle.vert, no gl_ClipDistance) → scissor to the doorway - // bbox indoors. Outdoors (skyDoorwayClip=false) draws full-screen. + // SkyPreScene particles (particle.vert, no gl_ClipDistance) — confined by the scissor. if (_particleSystem is not null && _particleRenderer is not null) - { - bool sc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene); - if (sc) _gl.Disable(EnableCap.ScissorTest); - } + + if (skySc) _gl.Disable(EnableCap.ScissorTest); } // K-fix1 (2026-04-26): suppress terrain + entity rendering while live mode is configured @@ -7546,10 +7553,11 @@ public sealed class GameWindow : IDisposable 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. + // Phase U.3: close the world-geometry clip bracket opened above. From here down the + // scene particles, debug lines, and UI use shaders that do NOT write gl_ClipDistance, so + // the planes must be OFF to avoid the undefined-behavior clip. (The weather/rain pass + // below DOES use sky.vert — it re-enables the planes in its OWN local bracket; the sky + // pre-scene pass above already did the same. Both are scissored to the doorway too.) for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); @@ -7574,6 +7582,13 @@ public sealed class GameWindow : IDisposable // outdoors). Suppressed in sealed dungeons / interiors with no exit portal in view. if (drawSkyThisFrame) { + // Scissor the WHOLE weather post-scene block (rain mesh + particles) to the doorway + // AABB when indoors — symmetric with the sky pre-scene block. The rain cylinder MESH + // is precisely clipped by sky.vert in Planes mode (scissor a harmless over-include); + // in Scissor mode (UBO count 0, no planes) the scissor is the ONLY confinement — else + // the 815m rain cylinder bleeds full-screen indoors. Outdoors → no scissor → unchanged. + bool wxSc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); + // Weather MESH (rain cylinder): re-bind binding=2 (the OutsideView UBO) defensively, // enable the 8 clip planes around RenderWeather, disable after. count==0 outdoors ⇒ // full-screen rain, unchanged. @@ -7586,15 +7601,12 @@ public sealed class GameWindow : IDisposable for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++) _gl.Disable(EnableCap.ClipDistance0 + _cp); - // SkyPostScene particles (particle.vert, no gl_ClipDistance) → scissor to the doorway - // bbox indoors, full-screen outdoors. + // SkyPostScene particles (particle.vert, no gl_ClipDistance) — confined by the scissor. if (_particleSystem is not null && _particleRenderer is not null) - { - bool sc = BeginDoorwayScissor(skyDoorwayClip, skyDoorwayNdc); _particleRenderer.Draw(_particleSystem, camera, camPos, AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene); - if (sc) _gl.Disable(EnableCap.ScissorTest); - } + + if (wxSc) _gl.Disable(EnableCap.ScissorTest); } // Debug: draw collision shapes as wireframe cylinders around the