[Fix] (DirectGLES, ShaderTranspiler): widen the formats SPIRV-Cross refuses to print even where GL_NV_image_formats exists

This commit is contained in:
2026-08-21 04:13:01 -04:00
parent 07d6277f87
commit 668f3e90c9
9 changed files with 209 additions and 76 deletions
@@ -728,8 +728,6 @@ namespace MobileGL {
LowerViewportIndexPass::DeclaresViewportIndexBuiltin(context.get());
features.DeclaresMultisampledImage =
ClampMultisampleFetchPass::DeclaresMultisampledImage(context.get());
features.DeclaresWidenableImageFormat =
WidenImageFormatsPass::DeclaresWidenableImageFormat(context.get());
return features;
}
@@ -776,10 +774,12 @@ namespace MobileGL {
bool ShaderCompiler::WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool onlyFormatsSpirvCrossRefusesToPrint,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(WidenImageFormatsPass::CreateWidenImageFormatsPass());
optimizer.RegisterPass(
WidenImageFormatsPass::CreateWidenImageFormatsPass(onlyFormatsSpirvCrossRefusesToPrint));
// Two image types that differed only in a format the widening collapses -
// `layout(rg32f)` and `layout(rgba32f)` in one module - are one type afterwards,
// and duplicate non-aggregate type declarations are invalid SPIR-V. This joins
@@ -791,8 +791,10 @@ namespace MobileGL {
true, enableSpirvValidation);
}
bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector<Uint32>& binary) {
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary);
bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
const bool onlyFormatsSpirvCrossRefusesToPrint) {
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary,
onlyFormatsSpirvCrossRefusesToPrint);
}
Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
@@ -71,10 +71,14 @@ namespace MobileGL {
// GL_OES_viewport_array AND integer multisample squeezed to 1) the separate
// probes made compile-heavy workloads measurably slower - ReservedNames-class
// CTS cases paid ~10%. Callers with more than one armed gate use this instead.
// The image-format widening deliberately does NOT ride this probe, even though it
// is a module question of exactly the same shape. It is armed on every driver, so
// a gate answered from the module would put a BuildModule on every stage of every
// program - and the frontend's uniform reflection can answer it for free
// (PrgramImpl::ImageFormatBakeInputs::declaresWidenableImageFormat).
struct SpirvGateFeatures {
Bool WritesViewportIndexOutput = false;
Bool DeclaresMultisampledImage = false;
Bool DeclaresWidenableImageFormat = false;
};
static SpirvGateFeatures ProbeSpirvGateFeatures(const Vector<Uint32>& binary);
// Replaces an ARRAY vertex input with one input per element at consecutive
@@ -241,15 +245,21 @@ namespace MobileGL {
// declared format natively. See WidenImageFormatsPass for the table, for the nine
// formats it deliberately does NOT widen, and for why the texture storage and the
// glBindImageTexture argument have to move with it.
// `onlyFormatsSpirvCrossRefusesToPrint` narrows it to the formats that have no
// ESSL route even WITH GL_NV_image_formats, because SPIRV-Cross throws for them
// rather than printing a token - which is the whole set a driver that advertises
// the extension still needs. See WidenImageFormatsPass.
static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool onlyFormatsSpirvCrossRefusesToPrint = false,
bool enableSpirvValidation = false);
// Whether the module declares a storage image WidenImageFormatsForEssl would
// widen, so the ~every shader that declares none pays no optimizer run. Costs its
// own module parse: the transpile path asks the same question through
// ProbeSpirvGateFeatures instead, because this gate is armed on every real driver
// and would otherwise put a BuildModule on every stage of every program.
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary);
// widen, under the same mode the run would use. Costs its own module parse, so
// the transpile path does NOT gate on this - it answers the question from the
// frontend's uniform reflection instead, for the reason on SpirvGateFeatures.
// Here for tests and for callers that already hold nothing but the binary.
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
bool onlyFormatsSpirvCrossRefusesToPrint = false);
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
// when it needs no widening or cannot be widened exactly. The single source of
// truth for all three layers of the emulation: this one answers the shader, and
@@ -8,6 +8,10 @@
#include "WidenImageFormatsPass.h"
// For IsSpirvCrossEsslPrintableFormat: the two passes share one question about the emitter, and
// the answer belongs where the rest of the image-format tables already are.
#include "BakeImageFormatsPass.h"
#include "spirv.hpp"
#include "source/opt/build_module.h"
#include "source/opt/constants.h"
@@ -211,12 +215,20 @@ namespace MobileGL {
}
}
Bool IsWidenableStorageImageType(const Instruction* type) {
Bool IsWidenableStorageImageType(const Instruction* type,
bool onlyFormatsSpirvCrossRefusesToPrint) {
if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false;
if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false;
const auto format =
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
return static_cast<Bool>(WideningOfSpirvImageFormat(format));
if (!WideningOfSpirvImageFormat(format)) return false;
if (onlyFormatsSpirvCrossRefusesToPrint &&
BakeImageFormatsPass::IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) {
// The driver can spell this one and the emitter will print it; widening it
// would spend two to four times the texture memory to change nothing.
return false;
}
return true;
}
} // namespace
@@ -231,23 +243,25 @@ namespace MobileGL {
return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
}
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(IRContext* context) {
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(
IRContext* context, const bool onlyFormatsSpirvCrossRefusesToPrint) {
if (context == nullptr) {
return false;
}
for (const Instruction& type : context->module()->types_values()) {
if (IsWidenableStorageImageType(&type)) {
if (IsWidenableStorageImageType(&type, onlyFormatsSpirvCrossRefusesToPrint)) {
return true;
}
}
return false;
}
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(const Vector<Uint32>& binary) {
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(
const Vector<Uint32>& binary, const bool onlyFormatsSpirvCrossRefusesToPrint) {
std::unique_ptr<IRContext> context = spvtools::BuildModule(
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
binary.data(), binary.size());
return DeclaresWidenableImageFormat(context.get());
return DeclaresWidenableImageFormat(context.get(), onlyFormatsSpirvCrossRefusesToPrint);
}
spvtools::opt::Pass::Status WidenImageFormatsPass::Process() {
@@ -258,7 +272,7 @@ namespace MobileGL {
// byte-identical - which is every shader but a handful.
std::vector<Instruction*> imageTypes;
for (Instruction& type : irContext->types_values()) {
if (IsWidenableStorageImageType(&type)) {
if (IsWidenableStorageImageType(&type, m_onlyFormatsSpirvCrossRefusesToPrint)) {
imageTypes.push_back(&type);
}
}
@@ -539,8 +553,10 @@ namespace MobileGL {
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken WidenImageFormatsPass::CreateWidenImageFormatsPass() {
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<WidenImageFormatsPass>());
spvtools::Optimizer::PassToken WidenImageFormatsPass::CreateWidenImageFormatsPass(
const bool onlyFormatsSpirvCrossRefusesToPrint) {
return spvtools::Optimizer::PassToken(
spvtools::MakeUnique<WidenImageFormatsPass>(onlyFormatsSpirvCrossRefusesToPrint));
}
} // namespace ShaderTranspiler
} // namespace MG_Util
@@ -74,19 +74,39 @@ namespace MobileGL {
// format from the same bind state, so the module must reach it unchanged.
class WidenImageFormatsPass final : public spvtools::opt::Pass {
public:
// `onlyFormatsSpirvCrossRefusesToPrint` narrows the pass to the formats that have
// no ESSL route even on a driver that DOES advertise GL_NV_image_formats.
// SPIRV-Cross's is_desktop_only_format set - r8ui, rg16f, r16i and fifteen others -
// makes it THROW for an ESSL target rather than print a token, and the throw takes
// the stage with it whatever the driver could have accepted. Mesa is exactly that
// case: it advertises the extension, so nothing else needs widening there, and
// `layout(r8ui) uimage2D` still lost its whole program until this ran for it.
//
// Off, the pass widens every format in the table, which is what a driver without
// the extension needs. The caller sets it from
// g_GLESCapabilities.SupportsExtendedImageFormats, and the SAME rule decides
// whether the ES texture storage and the glBindImageTexture argument widen
// (TextureImpl::GetImageBindableStorageWidening) - all three have to agree or the
// shader addresses a texel size the storage does not have.
explicit WidenImageFormatsPass(bool onlyFormatsSpirvCrossRefusesToPrint = false)
: m_onlyFormatsSpirvCrossRefusesToPrint(onlyFormatsSpirvCrossRefusesToPrint) {}
const char* name() const override { return "mobilegl-widen-image-formats"; }
Status Process() override;
// Whether the module declares a storage image whose format this pass would widen,
// i.e. whether running it could change anything. Answered from a single parse so
// the caller can skip the optimizer run entirely - which is every shader but a
// handful.
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary);
// handful. `onlyFormatsSpirvCrossRefusesToPrint` must match what the run will use,
// or the gate answers a question the pass is not being asked.
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary,
bool onlyFormatsSpirvCrossRefusesToPrint = false);
// The same question asked of a module the caller has ALREADY parsed, so a stage
// that has to answer several gate questions pays one BuildModule rather than one
// per gate - see ShaderCompiler::ProbeSpirvGateFeatures, and the ~10% it cost
// compile-heavy CTS cases when two gates each parsed for themselves.
static bool DeclaresWidenableImageFormat(spvtools::opt::IRContext* context);
static bool DeclaresWidenableImageFormat(spvtools::opt::IRContext* context,
bool onlyFormatsSpirvCrossRefusesToPrint = false);
// The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0
// when the format needs no widening (it is core already) or cannot be widened
@@ -99,7 +119,11 @@ namespace MobileGL {
// forty image formats. The count the widened accesses are masked back to.
static Uint ImageFormatChannelCount(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass();
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
bool onlyFormatsSpirvCrossRefusesToPrint = false);
private:
bool m_onlyFormatsSpirvCrossRefusesToPrint = false;
};
} // namespace ShaderTranspiler
} // namespace MG_Util