TilemapGPULayer: Fixing display issue on some Android devices (S23, ZFold 7, etc...) - #7349
Open
moufmouf wants to merge 1 commit into
Open
Conversation
uLayerSampler and uAnimSampler carry packed 32-bit integer records (tile index in RGB, flip/animation/empty flags in A), not colour, but were declared without a precision qualifier. GLSL ES 1.00 4.5.3 predeclares fragment shader samplers as lowp, which guarantees only 8-bit absolute precision. A driver that implements lowp literally returns the alpha channel slightly below its true value, so texel.a * 255.0 lands just under 16.0 and the bit-exact empty tile test `flags == 16.0` never matches. tile.empty is then false for every cell, getTileTexelCoord() returns vec2(0.0), and each empty cell draws tile 0 of the tileset - opaque in most tilesets - over everything beneath that layer. On a map with several stacked GPU layers sharing a tileset the topmost one wins and the map vanishes under a grid of whatever tile 0 happens to be. Measured on Adreno 740 (Galaxy S23, Chrome 150) with a standalone WebGL page that uploads a 1x1 RGBA8 texel with alpha byte 16, the value generateLayerDataTexture() writes for an empty tile: default (lowp) sampler: flags == 16.0 -> false, decoded 15.996 highp sampler: flags == 16.0 -> true, decoded 16.004 Desktop, Mali and older Adreno promote lowp to fp16/fp32, where 16/255 * 255 rounds back to exactly 16.0, which is why this only reproduces on some devices. Declare both data samplers via a DATA_PRECISION macro defined inside the existing GL_FRAGMENT_PRECISION_HIGH guard, so the shader still compiles where highp is unavailable in fragment shaders. uMainSampler is left at the default precision on purpose: it samples the tileset image as colour, where the default is correct and cheaper on mobile. Also add texelBytes(), snapping a fetched UNORM8 texel back to exact 0-255 values before anything decodes it, and route getLayerData() and floatTexel() through it. The channels there are weighted by powers of 256, so a sub-byte fetch error becomes an error of up to 65536 in the decoded tile index; the reporting device's drift was small enough to spare the index and break only the exact comparison, but nothing bounds it that tightly. Rounding also makes `flags == 16.0` exact rather than merely very likely to be exact. Note that rounding alone is not sufficient for the flag test as written, and neither is a threshold or bit test: the observed value 15.996 fails `== 16.0` and `>= 16.0` alike. Raising the sampler precision fixes it, rounding fixes it, and doing both makes the decode robust regardless of how a driver interprets sampler precision. Regenerates the bundled frag shader. Verified it compiles in a WebGL1 context with MAX_ANIM_FRAMES 0 and 8 and with FEATURE_BORDERFILTER, and that the reporting S23 renders the map correctly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR (delete as applicable)
Bug description:
We recently started using the new
TilemapGPULayerin production. While this works great on most computers, some users declared theTilemapGPULayerare not displayed at all on some Android devices (S23, S23 Ultra, ZFold 7, etc...), while still working on other Android devices (Pixel 9 Pro).Here is a display on the issue on a S23 phone:
We should not see the "Start" layer because it is hidden by a floor layer that is implemented with
TilemapGPULayerand not visible at all.Why this is happening only on some phones?
The issue comes from a rounding error that is not treated with the same precision depending on the phones.
TilemapGPULayer.fragdeclares its layer data sampler with no precision qualifier:uLayerSampler contains pure packed 32-bit integer record data (the tile number for each tile in the RGB part) and whether the tile is flipped / rotated / empty in alpha.
But sampler2D does not store integers. It's designed to store pixel colors, essentially as a float or double value.
On a driver that implements lowp literally, the fetched alpha comes back a fraction below
its true value, so texel.a * 255.0 lands just under 16.0 and the bit-exact empty tile
test never matches:
tile.emptyis then false for every cell in the layer. getTileTexelCoord() returnsvec2(0.0) for what it still believes is a real tile, so each empty cell samples tile 0
of the tileset — opaque in most tilesets — and paints it over everything drawn beneath
that layer. On a map with several stacked GPU layers sharing a tileset, the topmost one
wins and the map disappears under a grid of whatever tile 0 happens to be.
Reproduction
Standalone WebGL, no Phaser: upload a 1×1 RGBA8 texture with alpha byte 16 (exactly what
generateLayerDataTexture() writes for an empty tile), read it back through a fragment
sampler, and run texel.a * 255.0 == 16.0. The only difference between the two rows is the
sampler's precision qualifier.
Confirmed broken on Adreno 740/750 (Galaxy S23, S23 Ultra, Z Fold 7) and fine on desktop,
Mali (Pixel 9) and older Adreno/Xclipse (Galaxy S22) — those promote lowp to fp16/fp32,
where 16/255 * 255 rounds straight back to 16.0 and the bug is invisible. I'm happy to
attach the repro page if useful.
The fix
inside the existing GL_FRAGMENT_PRECISION_HIGH guard, so the shader still compiles
where highp is unavailable in fragment shaders. This is the load-bearing change.
uMainSampler is deliberately left at the default: it samples the tileset image as
colour, where the default precision is correct and cheaper on mobile.
anything decodes it, and route getLayerData() and floatTexel() through it. The
channels there are weighted by powers of 256, so a sub-byte fetch error becomes an error
of up to 65536 in the decoded tile index. The reporting device's drift was small enough
to spare the index and break only the exact comparison, but nothing bounds it that
tightly. This also makes flags == 16.0 exact rather than merely very likely to be
exact, which is why I left that comparison as-is.
Worth flagging one trap: changing == 16.0 to a threshold or bit test is not a fix on
its own. The observed value is 15.996, which fails >= 16.0 just as it fails == 16.0.
Raising the sampler precision fixes it, rounding fixes it; doing both makes the decode
robust regardless of how a driver interprets sampler precision.