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
--