From 5f08a5715c32ca6f8009fd851c649abcad7afdb8 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 06:14:15 -0400 Subject: [PATCH 1/4] The water discards at a mirror's plane after its derivatives are taken, not before waterF.glsl and underWaterF.glsl called mirrorClip first, which discards under MIRROR_CLIP, and then took derivatives: the wave normals' implicit-LOD reads and dFdx in water_wave_normals, and dFdx(pos) in the sun's shadow. A lane that discards leaves its quad, and GLSL leaves the derivatives of the lanes beside it undefined, so along a mirror's plane the reflected water read its normal maps at the wrong mip and filtered its shadow over the wrong footprint. Both test the plane with mirrorClipped at the top and discard at the end, as pbrterrainF.glsl does. The discard is compiled only in the mirror variant, so the water elsewhere keeps its early depth test. Co-Authored-By: Claude Opus 5.5 --- .../shaders/class3/environment/underWaterF.glsl | 14 ++++++++++++-- .../shaders/class3/environment/waterF.glsl | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl b/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl index fb3519a475..fbb54341a3 100644 --- a/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl +++ b/indra/newview/app_settings/shaders/class3/environment/underWaterF.glsl @@ -55,12 +55,13 @@ in vec3 view; in vec3 vary_position; vec4 applyWaterFogViewLinearNoClip(vec3 pos, vec4 color); -void mirrorClip(vec3 position); +bool mirrorClipped(vec3 position); void water_wave_normals(sampler2D map_a, sampler2D map_b, vec2 r, out vec3 big, out vec3 little_xy, out vec3 little_zw); void main() { - mirrorClip(vary_position); + // Discarded at the end: the wave normals take derivatives. + bool clipped = mirrorClipped(vary_position); vec2 screen_tc = gl_FragCoord.xy / screen_res; vec4 color; @@ -84,5 +85,14 @@ void main() fb = applyWaterFogViewLinearNoClip(vary_position, fb); + // Compiled only in the mirror variant: a discard the compiler cannot see is dead costs every fragment its early + // depth test. +#ifdef MIRROR_CLIP + if (clipped) + { + discard; + } +#endif + frag_color = max(fb, vec4(0)); } diff --git a/indra/newview/app_settings/shaders/class3/environment/waterF.glsl b/indra/newview/app_settings/shaders/class3/environment/waterF.glsl index d7696e4004..21e7d75168 100644 --- a/indra/newview/app_settings/shaders/class3/environment/waterF.glsl +++ b/indra/newview/app_settings/shaders/class3/environment/waterF.glsl @@ -40,7 +40,7 @@ float sampleDirectionalShadow(vec3 pos, vec3 norm, vec2 pos_screen); void calcAtmosphericVarsLinear(vec3 inPositionEye, vec3 norm, vec3 light_dir, out vec3 sunlit, out vec3 amblit, out vec3 atten, out vec3 additive); vec4 applyWaterFogViewLinearNoClip(vec3 pos, vec4 color); -void mirrorClip(vec3 pos); +bool mirrorClipped(vec3 pos); // PBR interface vec2 BRDF(float NoV, float roughness); @@ -130,7 +130,8 @@ void calculateFresnelFactors(out vec3 df3, out vec2 df2, vec3 viewVec, vec3 wave void main() { - mirrorClip(vary_position); + // Discarded at the end: the wave normals and the shadow take derivatives. + bool clipped = mirrorClipped(vary_position); // The plane's frame and the light in eye space, the same at every point: agent z and x through the view. vec3 vN = normal_matrix[2]; @@ -326,6 +327,15 @@ void main() } } + // Compiled only in the mirror variant: a discard the compiler cannot see is dead costs every fragment its early + // depth test. +#ifdef MIRROR_CLIP + if (clipped) + { + discard; + } +#endif + // Not capped at 1: the glint and bright reflections are HDR and have to reach white and bloom, and // what shows through the surface must match the shore beside it. The alpha is the glint's glow, which // the pool adds to the glow of what lies beneath. From 0d19e848cf8f43d848cd69491b7b314e2ac24675 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 06:14:17 -0400 Subject: [PATCH 2/4] The water haze leaves its depth test to the hardware From above, doWaterHaze draws the field with the hardware depth test on (GEQUAL under reverse-Z, writing none), and waterHazeF.glsl repeated the same test by hand and discarded. It did so because some GPUs once could not depth test against a target bound for sampling; the pass now reads the scene's depth past a texture barrier or from a copy (LLPipeline::hazeDepth), and the hardware test runs either way. The manual test, its second copy of the depth convention and the above_water uniform the fragment stage read for it are gone; the vertex stage still reads it. Co-Authored-By: Claude Opus 5.5 --- .../shaders/class3/deferred/waterHazeF.glsl | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/indra/newview/app_settings/shaders/class3/deferred/waterHazeF.glsl b/indra/newview/app_settings/shaders/class3/deferred/waterHazeF.glsl index 6b421511e3..48d196df16 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/waterHazeF.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/waterHazeF.glsl @@ -34,8 +34,6 @@ bool isFarDepth(float d); // deferredUtil.glsl -- depth-convention a vec4 getWaterFogView(vec3 pos); -uniform int above_water; - // Void water has no floor. The sky behind it keeps the cleared far depth, which an infinite projection // reconstructs a thousand km out and a finite one at its far plane, wherever that falls relative to the surface; // it is fogged instead as water this deep past where the ray enters, at any distance. @@ -45,28 +43,8 @@ uniform vec4 waterPlane; void main() { vec2 tc = vary_fragcoord.xy/vary_fragcoord.w*0.5+0.5; + // From above, the hardware depth test (LLPipeline::doWaterHaze) keeps the field off what lies in front of it. float depth = getDepth(tc.xy); - if (above_water > 0) - { - // we want to depth test when the camera is above water, but some GPUs have a hard time - // with depth testing against render targets that are bound for sampling in the same shader - // so we do it manually here - - // The matching hardware LLGLDepthTest in pipeline.cpp auto-translates its func under - // reverse-Z; keep this in sync. Under reverse-Z clip.z/clip.w is already the [0,1] - // ndc z (no *0.5+0.5), and "fragment behind stored geometry" is the smaller depth. -#ifdef REVERSE_Z - float cur_depth = vary_fragcoord.z/vary_fragcoord.w; - if (cur_depth < depth) -#else - float cur_depth = vary_fragcoord.z/vary_fragcoord.w*0.5+0.5; - if (cur_depth > depth) -#endif - { - discard; - } - - } vec4 pos = getPositionWithDepth(tc, depth); From 511d538ea82570b9e4da91b6c3eaed272310aaa6 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 06:15:01 -0400 Subject: [PATCH 3/4] The water haze is not drawn under water that is drawn opaque over it doWaterHaze fogs the scene under the surface, seen from above, by drawing the whole field over it. Where the view's water does not refract (ALRenderResourcePlan::refractsWater: RenderTransparentWater off, and every probe face), the water is drawn opaque straight after and hides all of it, so the pass read the depth, worked the fog and blended for nothing, once a frame and once a probe face. Where an exclusion prim removed the water the haze fogs nothing either (its NotExcluded stencil). Seen from below the pass is a full-screen fog of what lies in front of the camera and stays. Co-Authored-By: Claude Opus 5.5 --- indra/newview/pipeline.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 453c82752d..da39c15dca 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -12420,6 +12420,13 @@ void LLPipeline::doWaterHaze() return; } + // Nor where this view's water does not refract (ALRenderResourcePlan::refractsWater): its opaque surface hides what + // lies under it, which is all the haze fogs, and where an exclusion prim removed it the haze fogs nothing either. + if (!sUnderWaterRender && !ALRenderResourcePlan::refractsWater(renderPack(), sRenderTransparentWater)) + { + return; + } + if (RenderDeferredAtmospheric) { // The scene's depth as the haze reads it, after the pools before it wrote it in this binding. From d96fe275448617de44b9d5a2e539c3ed7b205600 Mon Sep 17 00:00:00 2001 From: Rye Date: Thu, 22 Oct 2026 06:18:34 -0400 Subject: [PATCH 4/4] The terrain tessellates at a density of 48 by default, not 64 AlchemyRenderTerrainTessDensity's default comes down from 64 to 48: a 16 m patch is split 48 ways (33 cm) within 16 m of the camera, 12 ways at 64 m, and not at all past 768 m, where 64 split it to 1 km. The triangle count grows with the square of the density, so the terrain draws a little over half the triangles it did, for an edge a third longer near the camera. The fallback in LLDrawPoolTerrain and the setting's worked example follow it; a saved value is kept. Co-Authored-By: Claude Opus 5.5 --- indra/newview/app_settings/settings_alchemy.xml | 4 ++-- indra/newview/lldrawpoolterrain.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 54224097ef..4f3f51cc00 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2446,13 +2446,13 @@ AlchemyRenderTerrainTessDensity Comment - Base density of the terrain tessellation: metres of patch edge per level, divided by the distance to the camera, so at 64 a 16 m patch is split 64 ways (25 cm) within 16 m of the camera, 16 ways (1 m) at 64 m and not at all past 1 km. The terrain detail slider multiplies it. Triangle count grows with the square; clamped to 8 to 256. + Base density of the terrain tessellation: metres of patch edge per level, divided by the distance to the camera, so at 48 a 16 m patch is split 48 ways (33 cm) within 16 m of the camera, 12 ways (1.3 m) at 64 m and not at all past 768 m. The terrain detail slider multiplies it. Triangle count grows with the square; clamped to 8 to 256. Persist 1 Type F32 Value - 64 + 48 AlchemyRenderTerrainTessRasterSegment diff --git a/indra/newview/lldrawpoolterrain.cpp b/indra/newview/lldrawpoolterrain.cpp index 757c27276a..cd6ce03f81 100644 --- a/indra/newview/lldrawpoolterrain.cpp +++ b/indra/newview/lldrawpoolterrain.cpp @@ -86,7 +86,7 @@ namespace // Pinned (density 0, which terrainTC reads as the grid's level) exactly when the programs were built with the // spacing that cuts the grid's level into its segments (TERRAIN_TESS_PINNED), from the same latched input, and // above 0 otherwise whatever a debug RenderTerrainLODFactor says. - static LLCachedControl tess_density(gSavedSettings, "AlchemyRenderTerrainTessDensity", 64.f); + static LLCachedControl tess_density(gSavedSettings, "AlchemyRenderTerrainTessDensity", 48.f); static LLCachedControl raster_segment(gSavedSettings, "AlchemyRenderTerrainTessRasterSegment", 8.f); const F32 density = gPipeline.shaderInputs().mLocal.mTerrainTessPinned ? 0.f