From 22b58e27a4d3f7cc24a67cfa4ee2a6739412cfd3 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Sat, 1 Aug 2026 00:13:01 -0400 Subject: [PATCH] fix android water shading --- CHANGELOG.md | 60 ++++++++++++++++++++++------------- lib/Water.lua | 23 ++++++++++++-- tests/dramatic_shape_test.lua | 11 +++++++ 3 files changed, 70 insertions(+), 24 deletions(-) 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: +- **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: - 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. + **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. - 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. + **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 --