mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Fix, Test] (ShaderTranspiler): keep a storage block with doubles at the byte layout it was bound with
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user