[Merge] (DirectGLES, ShaderTranspiler): take the viewport routing and image repairs under the fp64 and qualifier fixes

This commit is contained in:
2026-08-21 22:21:17 -04:00
18 changed files with 1338 additions and 122 deletions
@@ -39,6 +39,7 @@ namespace MobileGL {
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
// 5 Sampled, 6 Format.
constexpr uint32_t kImageSampledTypeOperand = 0;
constexpr uint32_t kImageDimOperand = 1;
constexpr uint32_t kImageSampledOperand = 5;
constexpr uint32_t kImageFormatOperand = 6;
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
@@ -50,16 +51,40 @@ namespace MobileGL {
constexpr uint32_t kImageAccessImageOperand = 0;
constexpr uint32_t kImageWriteTexelOperand = 2;
// The exact carrier of a non-core image format: the core GLSL ES format with the
// SAME component type and the SAME per-channel width, differing only in channel
// count. `channels` is what the original format really has, which is what every
// access through the carrier is masked back to.
// The carrier of a non-core image format: a core GLSL ES format that represents
// every value the original can hold, WITHOUT LOSS. `channels` is what the original
// format really has, which is what every access through the carrier is masked back
// to.
//
// Only formats that widen EXACTLY appear here. r11f_g11f_b10f, rgb10_a2,
// rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm and r16_snorm have no
// same-width core carrier - every candidate is either lossy or changes the numeric
// domain a sampler would read - and are deliberately absent, so they keep the
// honest "no GLSL ES spelling" diagnostic rather than a silent approximation.
// Almost every entry is a pure CHANNEL widening - same component type, same
// per-channel width, more channels (rg32f -> rgba32f) - and for those the carrier
// is bit-exact: the storage holds the identical encoding, only wider.
//
// r11f_g11f_b10f is the one entry that is not. It has no same-width core carrier,
// so it takes rgba16f, and the two encodings differ. What matters is that the
// carrier is still LOSSLESS: an 11-bit float is e5m6 and a 10-bit float is e5m5,
// while a half is s1e5m10 - the SAME 5-bit exponent with a strictly longer
// mantissa - so every value the packed format can represent has an exact half.
// Nothing an application stores is rounded away.
//
// What DOES change is the reverse direction: the carrier can hold values the
// packed format could not - negatives (11f and 10f are unsigned), and mantissa
// bits finer than the 6 and 5 the format quantises to - so a value written through
// the image and then SAMPLED comes back on half's grid rather than the packed
// format's. That is a strictly finer grid, never a lossy one, and it is measured
// against the alternative, which is not a more faithful quantisation but no
// program at all: `layout(r11f_g11f_b10f)` has no ESSL spelling, SPIRV-Cross
// throws for it, and the stage - with every other image uniform declared beside it
// - is lost (KHR-GL43.shader_image_load_store.basic-allFormats-*, which fail on
// this format alone, and multiple-uniforms, where one such declaration killed a
// program holding eight images).
//
// The remaining eight - rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm,
// rg16_snorm and r16_snorm - stay absent, and for a stronger reason than
// quantisation: core ESSL has no 16-bit normalized format at all and no 10-bit
// one, so every candidate carrier for them either loses range or changes the
// component TYPE the texture presents. They keep the honest "no GLSL ES spelling"
// diagnostic rather than a silent approximation.
struct ImageFormatWidening {
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
uint32_t Channels = 0;
@@ -73,6 +98,9 @@ namespace MobileGL {
case spv::ImageFormat::Rg32f: return {spv::ImageFormat::Rgba32f, 2};
case spv::ImageFormat::Rg16f: return {spv::ImageFormat::Rgba16f, 2};
case spv::ImageFormat::R16f: return {spv::ImageFormat::Rgba16f, 1};
// Not a channel widening but a lossless re-encoding - see above. Three
// channels, so the fourth reads as the 1 GL defines for a format without one.
case spv::ImageFormat::R11fG11fB10f: return {spv::ImageFormat::Rgba16f, 3};
// Unsigned normalized.
case spv::ImageFormat::Rg8: return {spv::ImageFormat::Rgba8, 2};
case spv::ImageFormat::R8: return {spv::ImageFormat::Rgba8, 1};
@@ -219,6 +247,24 @@ namespace MobileGL {
bool onlyFormatsSpirvCrossRefusesToPrint) {
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false;
// A BUFFER image is never widened, whatever its format. Widening works because
// the ES texture behind the image can be REALLOCATED in the carrier, so the
// texel the shader addresses and the texel the storage holds stay the same
// size. A buffer image has no storage of its own to reallocate: its texels are
// the application's buffer object, at the size and layout the application gave
// it, and that buffer is usually also a vertex, index or storage buffer whose
// contents are not ours to relayout.
//
// Widening one anyway makes the shader stride 16 bytes through 8-byte texels.
// Measured on an Adreno 830 with a 32-byte GL_RG32F buffer and a shader storing
// (i+1, 100) at texel i: the readback came back [1,100] [0,1] [2,100] [0,1] -
// texels 0 and 1 landed on top of all four, texels 2 and 3 ran off the end of
// the application's buffer. Declining leaves the honest "no GLSL ES spelling"
// failure instead, which loses the same stage but corrupts nothing.
if (static_cast<spv::Dim>(type->GetSingleWordInOperand(kImageDimOperand)) ==
spv::Dim::Buffer) {
return false;
}
const auto format =
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
if (!WideningOfSpirvImageFormat(format)) return false;
@@ -54,12 +54,23 @@ namespace MobileGL {
// alone survives storage this shader never wrote (glTexStorage with no upload, whose
// surplus channels are undefined).
//
// The other NINE (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16, r16,
// rgba16_snorm, rg16_snorm, r16_snorm) have NO same-width core carrier and are
// deliberately NOT widened here: every carrier for them is either lossy or changes the
// numeric domain of the texture a `sampler2D` would read from it. They keep the honest
// "no GLSL ES spelling" diagnostic instead of silently changing an application's
// quantisation behaviour.
// r11f_g11f_b10f has no same-width core carrier either, and takes rgba16f anyway,
// because that carrier is still LOSSLESS: 11f is e5m6 and 10f is e5m5 against a half's
// s1e5m10 - the SAME 5-bit exponent with a strictly longer mantissa - so every value
// the packed format can hold has an exact half. Only the reverse direction differs
// (the carrier also holds negatives, which 11f and 10f cannot sign, and mantissa bits
// finer than the 6 and 5 they quantise to, so a value written through the image and
// then SAMPLED lands on half's grid rather than the packed format's). That is measured
// against the alternative, which is not a truer quantisation but no program at all:
// the SPIRV-Cross throw takes the whole stage, every image uniform declared beside it
// included.
//
// The other EIGHT (rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm,
// r16_snorm) are deliberately NOT widened here: core ESSL has no 16-bit normalized
// format at all and no 10-bit one, so every carrier for them either loses range or
// changes the component TYPE the texture a `sampler2D` would read presents. They keep
// the honest "no GLSL ES spelling" diagnostic instead of silently changing an
// application's numeric domain.
//
// MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the
// ES texture behind the image has to be allocated in the carrier format too, and
@@ -169,7 +169,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
TranslationKeyBuilder builder;
AppendCommonKeyPrefix(builder, kEsslKeyTag);
builder.Value(static_cast<Uint32>(inputs.shaderType));
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
builder.Value(static_cast<Uint8>(inputs.viewportIndexLoweringArmed));
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
builder.Value(inputs.maxColorTextureSamples);
@@ -556,7 +556,8 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// * the GL stage enum - three passes are stage-gated (draw parameters and
// array vertex inputs on vertex, fragment-output index legalization on
// fragment);
// * SupportsViewportArray - arms LowerViewportIndexForEssl;
// * the viewport-index lowering arming bit - GL_OES_viewport_array's absence OR the
// routing emulation being on - which arms LowerViewportIndexForEssl;
// * the four sample ceilings (color / integer / depth / advertised) - both
// ARM ClampMultisampleFetchesForEssl and PARAMETERIZE it;
// * SupportsNoperspectiveInterpolation - arms EmulateNoPerspectiveForEssl;
@@ -583,7 +584,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// armed by nothing, so the SPIR-V already in this key covers them completely.
//
// THE TEST FOR THAT CLAIM IS NOT THE SIGNATURE. LowerViewportIndexForEssl is equally
// module-only to look at, yet SupportsViewportArray is in this key because that bit ARMS
// module-only to look at, yet the arming bit is in this key because it ARMS
// it at the call site. So a new pass needs BOTH checks - what it takes, and what decides
// whether it runs - before "no key material" is a conclusion rather than an assumption.
// Note also where an application-authored value can hide: the atomic-counter
@@ -612,7 +613,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
GLenum shaderType = 0;
// --- driver capability bits that arm or steer a pass ---
Bool supportsViewportArray = false;
// Whether LowerViewportIndexForEssl runs on this module. NOT the raw
// GL_OES_viewport_array bit any more: the routing emulation arms the pass even where the
// extension exists (Config.h, ViewportArrayEmulation), so the extension alone no longer
// decides, and a key carrying only it would serve a module lowered under one setting to a
// link made under the other.
Bool viewportIndexLoweringArmed = false;
Bool supportsNoperspectiveInterpolation = false;
// GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage
// image whose format GLSL ES core cannot spell in the core format that carries it and