[Fix, Test] (ShaderTranspiler): keep a storage block with doubles at the byte layout it was bound with

This commit is contained in:
2026-08-21 22:15:05 -04:00
parent 7b0f443d3a
commit 348a30a816
8 changed files with 1770 additions and 37 deletions
@@ -19,6 +19,7 @@
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/DemoteFloat64Pass.h"
#include "SpirvPasses/FlattenFloat64StorageBlockPass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/LowerViewportIndexPass.h"
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
@@ -797,6 +798,17 @@ namespace MobileGL {
// in particular it runs before the backends' PackDoubleVertexInputsPass, whose
// OpBitcast this one would otherwise decline on. Costs one types_values() walk on
// the overwhelming majority of modules, which declare no 64-bit float at all.
// ...but demoting a double that lives in a SHADER STORAGE BLOCK also repacks that
// block, and the bytes an application put in the buffer do not move with it. This
// runs first and takes those blocks out of the demotion's hands: each becomes a
// flat `uint` array whose index arithmetic carries the std140/std430 offsets
// glslang computed WITH the doubles in place, so the layout survives byte for byte
// and only the VALUES narrow. Gated on a block actually holding a 64-bit float, so
// every other module pays one types_values() walk and nothing else, and it declines
// (leaving the block for the demotion to handle the old way) on any shape it cannot
// re-address exactly. See FlattenFloat64StorageBlockPass.h.
optimizer.RegisterPass(
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass());
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
@@ -50,12 +50,10 @@ namespace MobileGL {
// for the same reason - writes exactly where the demoted shader reads. Blocks with no
// 64-bit member anywhere are never touched.
//
// THE MEASURED COST, so the next wave does not re-diagnose it. Four GL 4.3 conformance
// cases fail on BOTH backends and on both an Adreno 830 and a Mali G925 - i.e. on every
// device, because no device has shaderFloat64 and the demotion therefore always runs:
// WHAT RE-DERIVING STILL COSTS, so the next wave does not re-diagnose it. Two GL 4.3
// conformance cases fail on BOTH backends and on every device, because no device has
// shaderFloat64 and the demotion therefore always runs:
//
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-cs
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3-vs
// KHR-GL43.compute_shader.fp64-case1
// KHR-GL43.compute_shader.fp64-case3
//
@@ -63,44 +61,28 @@ namespace MobileGL {
// against it: it is blocked on GLSL subroutines ("FP64 support - subroutines"), which
// glslang deletes when targeting SPIR-V, and is out of scope by standing instruction.
//
// The other three fail in the two ways this comment predicts and in no other.
// stdLayout-case3 copies a block byte for byte: the output matches the input for
// bytes [0, 76) and is zero from there on, which is exactly the block's size once
// every double became a float and the layout repacked tightly. Re-derived byte-exactly
// in 2026-08: the block is `int data0; float data1[5]; mat3x2 data2; double data3;
// double data4[2]; int data5; dvec3 data6`, and demoting every double to float and
// repacking std430 gives data0@0, data1@4..23, data2@24..47, data3@48, data4@52..59,
// data5@60, data6@64..75 - 76 bytes. EVERY mismatching byte the QPA reports is >= 76
// and every expected-non-zero byte below 76 matched, on both the std140 output and the
// std430 one.
//
// ONE TRAP FOR THE NEXT READER, because it reads as evidence AGAINST demotion and is
// not: in the std430 output the doubles below the boundary appear to have round-tripped
// BIT-EXACTLY, which looks like fp64 surviving. It is an artifact. The shader reads and
// writes through the SAME demoted offset, so those four bytes are copied verbatim
// whatever they are interpreted as - the copy proves nothing about the width.
//
// fp64-case1 reports ceil(2.2) as 2: the uniform's double 2.0 is 0x4000000000000000,
// the demoted read takes its low 32 bits (0.0), ceil(0.0 + 0.2) = 1.0f = 0x3F800000
// lands in the low half of the 8-byte output slot and the whole thing prints as 2.
// Index 0 of the same case PASSES by accident, for the same reason - writing 0.0f into
// the low half of 1.0 leaves it unchanged - so a partial pass here is not progress.
// Fixing it means carrying a double in the DEFAULT UNIFORM block without re-deriving
// its layout, and that block's routing is built by reflecting the module this pass
// produces, so the representation change ripples into every glUniform*d. Deliberately
// not attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
// green.
//
// Both backends produce a CHARACTER-FOR-CHARACTER identical QPA byte list, which is
// the cheapest available proof that the defect is in this shared pass and in neither
// backend. A future wave that wants to re-open this should start by re-checking that
// identity rather than by re-deriving the layout.
//
// Fixing them means NOT demoting a double that lives in a buffer block, and carrying
// it as a uvec2 word pair instead - preserving the application's byte layout exactly,
// unpacking to fp32 for arithmetic and repacking on store. That is a large pass with
// the same dmat problem the paragraph above describes (a uvec2 representation cannot
// express a matrix stride either, so it would have to decline dmat types), and the
// default-uniform routing above reflects the demoted module, so a representation
// change there ripples into every glUniform*d. THREE actionable cases of 16085 (the
// fourth, fp64-case3, is subroutine-blocked and unreachable from here); deliberately
// not attempted, and re-confirmed as not worth attempting in the 2026-08 wave.
// compute_shader.fp64-case2 passes today and any attempt has to keep it green.
// SHADER STORAGE BLOCKS ARE NO LONGER IN THAT LIST, and the two cases that used to be
// (shader_storage_buffer_object.basic-stdLayout-case3-cs and -vs, which copy a block
// byte for byte and used to come back zero from the first double's slot onwards) pass
// on both backends. FlattenFloat64StorageBlockPass runs immediately before this one
// and takes every storage block holding a 64-bit float out of its hands, rewriting the
// block into a flat `uint` array whose index arithmetic carries the offsets glslang
// computed WITH the doubles in place. A flat array has no layout for SPIRV-Cross to
// re-derive, which is what makes it expressible where a padded struct is not, and an
// offset in an address computation has none of the dmat trouble the paragraph above
// describes. See that pass's header. Everything below still describes what happens to
// every OTHER block, and to the doubles in the function bodies of all of them.
//
// Declines (leaves the module byte-identical, so the caller's existing "this module
// still declares Float64" failure path reports it) when the module contains an
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Rewrites a SHADER STORAGE BLOCK that contains a 64-bit float into a flat
// `uint` word array, and turns every access to it into address arithmetic over
// that array. The application's byte layout survives exactly; the VALUES are
// still narrowed to 32-bit floats, because that is all any target here has.
//
// WHY THIS EXISTS. DemoteFloat64Pass rewrites `double` to `float` in place and
// lets SPIRV-Cross re-derive the block's packing from the declared types, because
// GLSL ES has no member `layout(offset=)` and SPIRV-Cross refuses any block whose
// stated offsets it cannot express as std140 or std430. That re-derivation moves
// every member past the first double: the block a shader reads and writes stops
// being the block the application filled. Byte-for-byte, on the shape
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3 uses, the output
// matched the input up to the first double's slot and was zero from there on -
// the demoted block is simply shorter than the one that was bound.
//
// A flat `uint[]` has no layout to re-derive: one member, offset 0, ArrayStride 4,
// which IS std430, so SPIRV-Cross prints it unconditionally and the driver lays it
// out the only way it can. Every member's real byte offset - the std140 or std430
// one glslang computed WITH the doubles in place - then lives in the index
// arithmetic this pass emits, not in the declaration. The two ways the earlier
// attempt at this was blocked both disappear with it:
//
// * dmat: a `uvec2`-per-double representation cannot express a MatrixStride, so
// it would have had to decline matrices of doubles. Here a stride is a number
// in an address computation and nothing else, so dmat needs no special case.
// * the default-uniform block: its routing is built by reflecting the DEMOTED
// module (ProgramSpirvTask::BuildGlobalUboRouting), so changing how a double
// is carried there would ripple into every glUniform*d. This pass touches
// StorageBuffer blocks only and never that one.
//
// WHAT GL SEES IS UNCHANGED, and becomes CORRECT rather than merely unchanged:
// glGetProgramResourceiv answers from glslang's reflection of the pre-demotion
// module (ProgramInterface.cpp reads TObjectReflection::offset), i.e. the true
// fp64 offsets. Before this pass those offsets described a layout no shader used;
// now they describe the one it does.
//
// PRECISION, stated plainly. A double still becomes a float: the load narrows the
// stored binary64 to binary32 and the store widens it back, so a value that does
// not survive a round trip through 32 bits does not survive this either. The
// narrowing truncates the discarded mantissa bits rather than rounding to nearest,
// and flushes what binary32 can only hold as a subnormal to a signed zero; NaN
// stays NaN and an out-of-range magnitude becomes an infinity. That is the same
// fp32 promise DemoteFloat64Pass already makes - what changes is only that the
// BYTES around the value stay where the application put them.
//
// DECLINES, leaving the block exactly as it was for DemoteFloat64Pass to handle the
// old way, whenever it meets something it cannot rewrite exactly:
// - a block whose variable is used as anything but an access-chain base (loaded
// whole, handed to a function, asked its OpArrayLength);
// - an access chain that is not rooted at the variable, or whose result feeds
// anything but a plain OpLoad / OpStore (an atomic, OpCopyMemory, a further
// chain);
// - a non-constant index into a struct, a runtime array anywhere in the block, a
// RowMajor matrix (its columns are not contiguous, so a whole-column access is
// not one range), a member width other than 32 or 64 bits, or an offset or
// stride that is not a multiple of 4;
// - a load or store whose type decomposes into more scalars than the cap below,
// so legalizing a block can never explode the module.
//
// ORDERING: must run BEFORE DemoteFloat64Pass, which is what turns the doubles this
// pass leaves in the function body into floats - the OpFConvert pairs emitted here
// are width-preserving by then and collapse to their operands. It emits only 32-bit
// OpBitcasts, so it never trips that pass's "bitcast across the 64-bit boundary"
// decline.
class FlattenFloat64StorageBlockPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "mobilegl-flatten-float64-storage-block"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateFlattenFloat64StorageBlockPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL