DramaticShapeVoxelMod archival mirror · read-only
22b58e27a4d3f7cc24a67cfa4ee2a6739412cfd3

fix android water shading

DramaticShape · 2026-08-01 · parent 98f7419b72

fix android water shading

Changed files

CHANGELOG.md                  | 64 +++++++++++++++++++++++++++++++++++++++------------------------
 lib/Water.lua                 | 23 +++++++++++++++++++++--
 tests/dramatic_shape_test.lua | 11 +++++++++++
 3 files changed, 72 insertions(+), 26 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3511684..952ec1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -149,30 +149,46 @@
 
 ### Fixed
 
-- **On Android the water stayed flat, as if the row were off.** Two GLSL ES
-  defaults the desktop never exercises, both in the water shader:
-
-  Fragment floats default to **mediump** on GLSL ES, and the vertex stage's
-  default is highp -- and the water shader is the mod's first to declare the
-  same uniform, the frame's `vp` matrix, in BOTH stages, one on each default.
-  GLSL ES refuses to link a uniform whose precision the stages disagree on,
-  so the whole shader failed to build and the pass fell back -- quietly, by
-  design -- to the flat water the mode always drew. The pixel stage now lifts
-  its float default to highp (guarded, so a GPU without fragment highp still
-  compiles and falls back flat), which settles the link and is also simply
-  needed: the march works in world coordinates that run to a few thousand,
-  where fp16 has no fraction left, and the world-position varying is
-  qualified highp for the same reason the wireframe's has been all along.
-  Samplers default to **lowp** no matter what floats are set to, so the depth
-  read is lifted too -- eight bits of depth is a march with nothing to land
-  on.
-
-  And the readable depth canvas -- the one hardware requirement the rest of
-  the mode does not already have -- now tries four formats before giving up:
-  depth24, depth24 riding a stencil (a pairing some mobile drivers will
-  texture when they refuse the bare format), depth32f, and depth16 as the
-  floor every GLES3 device can read. Refused all four, the reflections are
-  lost and nothing else, exactly as before.
+- **On Android the water stayed flat, as if the row were off -- and once it
+  did draw, it came up in blocks with the haze showing through the holes.**
+  Three separate faults, every one of them invisible on desktop GL, run down
+  on a Galaxy Z Fold 7 with the driver's own compiler errors in logcat:
+
+  **The shader would not build.** Fragment floats default to **mediump** on
+  GLSL ES while the vertex stage's default is highp, and the water shader is
+  the mod's first to declare the same uniform -- the frame's `vp` matrix --
+  in BOTH stages, one on each default; GLSL ES refuses to link that, and the
+  pass fell back, quietly and by design, to the flat water the mode always
+  drew. The pixel stage now lifts its float default to highp (guarded, so a
+  GPU without fragment highp still compiles and falls back flat), which
+  settles the link and is also simply needed: the march works in world
+  coordinates that run to a few thousand, where fp16 has no fraction left.
+  The world-position varying is qualified highp for the same reason the
+  wireframe's always was, and the depth sampler too -- samplers default to
+  **lowp** whatever the floats are set to, and eight bits of depth is a
+  march with nothing to land on. One wrinkle inside the fix: LOVE's header
+  forward-declares `effect()` under ITS default, and Samsung's Xclipse
+  compiler treats a definition whose parameter precisions have drifted from
+  the prototype's as an illegal overload -- so effect()'s own float
+  parameters stay pinned to mediump, matching the declaration, and the
+  maths above them runs highp regardless.
+
+  **The depth test read the wrong texels.** The shader's own depth test
+  normalised LOVE's pixel coordinate by the `screen` uniform, which counts
+  canvas UNITS -- and on a highdpi phone (Android's density here is 2.625)
+  a canvas holds that many PIXELS per unit, so the lookup ran to 2.6,
+  clamped, and read edge texels across two thirds of the frame. Water
+  discarded itself in blocks wherever the mis-read depth landed in front,
+  and the haze backdrop showed through the holes. The coordinate is now
+  normalised by `love_ScreenSize.xy` -- the bound canvas's own pixel size,
+  measured in the same units on every display.
+
+  **And the readable depth canvas** -- the one hardware requirement the
+  rest of the mode does not already have -- now tries four formats before
+  giving up: depth24, depth24 riding a stencil (a pairing some mobile
+  drivers will texture when they refuse the bare format), depth32f, and
+  depth16 as the floor every GLES3 device can read. Refused all four, the
+  reflections are lost and nothing else, exactly as before.
 
 ### Known
 
diff --git a/lib/Water.lua b/lib/Water.lua
index 77a32e6..b7ce4f8 100644
--- a/lib/Water.lua
+++ b/lib/Water.lua
@@ -849,12 +849,31 @@ vec2 waveUV(vec2 tc, vec2 col) {
   return org + (mod(col, 8.0) + 0.5) * texel;
 }
 
-vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
+// The float parameters are pinned to mediump BECAUSE the stage default is
+// not: LOVE's own header forward-declares effect() under its default, and
+// at least one mobile compiler (Samsung's Xclipse, in so many words) holds
+// that a definition whose parameter precisions differ from its prototype's
+// is a second function of the same name, and refuses the pair. The params
+// can afford it -- the colour is a colour, and tc/sc arrived through
+// LOVE's mediump plumbing whatever this signature says -- and the maths
+// below runs on the stage default the moment the values touch a local.
+vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
   // THE DEPTH TEST, done here because the buffer that would have done it is
   // detached for the length of this pass so it can be READ (see the header).
   // Same comparison, same buffer, same result: a building in front of a pond
   // still hides it.
-  vec2 uv = sc / screen;
+  //
+  // Normalised by LOVE's own screen size, not by the `screen` uniform: `sc`
+  // arrives in canvas PIXELS, and on a highdpi surface (Android's density
+  // is routinely 2.625) a canvas holds that many pixels per canvas UNIT,
+  // which is what `screen` counts. Divided by units, uv runs to 2.6 and
+  // clamps, and the test reads edge texels for two thirds of the frame --
+  // discarding water in blocks and letting the haze backdrop through, which
+  // on a phone looked like lakes with pieces missing. love_ScreenSize.xy is
+  // the bound canvas's own pixel size, the same units sc is measured in, on
+  // every display. (`screen` stays in units: skyPos reads it against cell
+  // and skyEdge, which are unit-measured with it.)
+  vec2 uv = sc / love_ScreenSize.xy;
   if (gl_FragCoord.z > Texel(depthTex, uv).r) discard;
 
   // THE COLUMN THIS FRAGMENT IS LOOKING AT. Every water pixel is a bar of
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 2f8bd76..27930f3 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -1796,6 +1796,17 @@ T.check(plain:find("LOVE_HIGHP_OR_MEDIUMP vec3 vBent", 1, true) ~= nil,
   .. "rather than left to the fragment default")
 T.check(plain:find("LOVE_HIGHP_OR_MEDIUMP Image depthTex", 1, true) ~= nil,
   "and the depth sampler is lifted off lowp, which is eight bits of depth")
+T.check(plain:find(
+    "effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc)",
+    1, true) ~= nil,
+  "effect()'s own floats stay pinned to LOVE's prototype precision -- the "
+  .. "Xclipse compiler reads a definition that drifted from the forward "
+  .. "declaration as an illegal overload and refuses the whole shader")
+T.check(plain:find("sc / love_ScreenSize.xy", 1, true) ~= nil,
+  "the depth test normalises the pixel coord by the canvas's own pixel "
+  .. "size -- `screen` counts canvas UNITS, and on a highdpi phone the two "
+  .. "differ by the density, which clamped the lookup and cut the water "
+  .. "into blocks")
 
 -- ------- the lift itself
 --