mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (DirectGLES, ShaderTranspiler): emulate the 17 exactly-carriable non-core image formats by channel widening
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
#include "SpirvPasses/WidenImageFormatsPass.h"
|
||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -771,6 +772,35 @@ namespace MobileGL {
|
||||
BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat));
|
||||
}
|
||||
|
||||
bool ShaderCompiler::WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(WidenImageFormatsPass::CreateWidenImageFormatsPass());
|
||||
// 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
|
||||
// them, and cascades to the pointer and array types that named them; the pass
|
||||
// itself deliberately does not carry a join of its own.
|
||||
optimizer.RegisterPass(CreateRemoveDuplicatesPass());
|
||||
|
||||
return RunOptimizerChecked("WidenImageFormatsForEssl", optimizer, inputBinary, outputBinary,
|
||||
true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector<Uint32>& binary) {
|
||||
return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::WidenedCoreEsslImageFormat(glInternalFormat);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::ImageFormatChannelCount(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
|
||||
@@ -231,6 +231,31 @@ namespace MobileGL {
|
||||
// must not ask BakeImageFormatsForEssl for those, and completes them in the
|
||||
// emitted text instead.
|
||||
static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat);
|
||||
// Re-declares every storage image whose DECLARED format GLSL ES cannot spell in
|
||||
// the core format that carries it exactly, and masks each access back to the
|
||||
// channels the original format has. The 26 formats outside the ES core set have no
|
||||
// legal ESSL spelling on any tested driver (none exposes GL_NV_image_formats), and
|
||||
// a format-less declaration is rejected too, so the stage is otherwise lost
|
||||
// whatever this backend emits. DirectGLES transpile path only - Vulkan takes the
|
||||
// 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.
|
||||
static bool WidenImageFormatsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares a storage image WidenImageFormatsForEssl would
|
||||
// widen. One module parse, so the ~every shader that declares none pays no
|
||||
// optimizer run.
|
||||
static bool DeclaresWidenableImageFormat(const Vector<Uint32>& binary);
|
||||
// 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
|
||||
// DirectGLES asks it again for the texture storage and the image bind, so the two
|
||||
// sides cannot drift.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
// Channels a GL image internal format really has (1-4), 0 when it is not one of
|
||||
// the forty image formats.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "BakeImageFormatsPass.h"
|
||||
|
||||
#include "WidenImageFormatsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
@@ -418,7 +420,18 @@ namespace MobileGL {
|
||||
// Those formats are completed in the emitted text instead (see
|
||||
// PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for
|
||||
// them, which is exactly the state that pass looks for.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format))) continue;
|
||||
//
|
||||
// UNLESS the format widens exactly: WidenImageFormatsPass runs immediately
|
||||
// after this one on the ESSL chain and rewrites it to a core four-channel
|
||||
// carrier SPIRV-Cross does print, masking the accesses back to the channels
|
||||
// the baked format has. So for those the module IS the right place, and
|
||||
// routing them to the text completion instead would spell the narrow format
|
||||
// the driver rejects. The two lists are asked in this order because
|
||||
// printability is the cheaper and more common answer.
|
||||
if (!IsSpirvCrossEsslPrintableFormat(static_cast<Uint32>(format)) &&
|
||||
WidenImageFormatsPass::WidenedCoreEsslImageFormat(formatIt->second) == 0) {
|
||||
continue;
|
||||
}
|
||||
// spirv-val: "Expected Image Format to match Sampled Type". A bind format
|
||||
// whose class disagrees with the declaration is an application error GL
|
||||
// leaves undefined; baking it would turn that into an invalid module, so it
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||
// Copyright (c) 2025-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
|
||||
|
||||
#include "WidenImageFormatsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
// 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 kImageSampledOperand = 5;
|
||||
constexpr uint32_t kImageFormatOperand = 6;
|
||||
// A storage image, i.e. one reached through imageLoad/imageStore rather than a
|
||||
// sampler. The only kind that carries a format qualifier in any GLSL dialect.
|
||||
constexpr uint32_t kSampledStorageImage = 2;
|
||||
|
||||
// OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands.
|
||||
// OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional.
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
struct ImageFormatWidening {
|
||||
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
|
||||
uint32_t Channels = 0;
|
||||
|
||||
explicit operator bool() const { return Carrier != spv::ImageFormat::Unknown; }
|
||||
};
|
||||
|
||||
ImageFormatWidening WideningOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
// Float.
|
||||
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};
|
||||
// Unsigned normalized.
|
||||
case spv::ImageFormat::Rg8: return {spv::ImageFormat::Rgba8, 2};
|
||||
case spv::ImageFormat::R8: return {spv::ImageFormat::Rgba8, 1};
|
||||
// Signed normalized.
|
||||
case spv::ImageFormat::Rg8Snorm: return {spv::ImageFormat::Rgba8Snorm, 2};
|
||||
case spv::ImageFormat::R8Snorm: return {spv::ImageFormat::Rgba8Snorm, 1};
|
||||
// Signed integer.
|
||||
case spv::ImageFormat::Rg32i: return {spv::ImageFormat::Rgba32i, 2};
|
||||
case spv::ImageFormat::Rg16i: return {spv::ImageFormat::Rgba16i, 2};
|
||||
case spv::ImageFormat::R16i: return {spv::ImageFormat::Rgba16i, 1};
|
||||
case spv::ImageFormat::Rg8i: return {spv::ImageFormat::Rgba8i, 2};
|
||||
case spv::ImageFormat::R8i: return {spv::ImageFormat::Rgba8i, 1};
|
||||
// Unsigned integer.
|
||||
case spv::ImageFormat::Rg32ui: return {spv::ImageFormat::Rgba32ui, 2};
|
||||
case spv::ImageFormat::Rg16ui: return {spv::ImageFormat::Rgba16ui, 2};
|
||||
case spv::ImageFormat::R16ui: return {spv::ImageFormat::Rgba16ui, 1};
|
||||
case spv::ImageFormat::Rg8ui: return {spv::ImageFormat::Rgba8ui, 2};
|
||||
case spv::ImageFormat::R8ui: return {spv::ImageFormat::Rgba8ui, 1};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// The GL 4.2 image format table (core spec table 8.26) as SPIR-V ImageFormats.
|
||||
// Written as literals rather than through the GL headers because this lives in
|
||||
// MG_Util, which the GL frontend's enums do not reach; the same list, in the same
|
||||
// order, as BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat.
|
||||
spv::ImageFormat SpirvImageFormatOfGL(Uint glInternalFormat) {
|
||||
switch (glInternalFormat) {
|
||||
case 0x8814: /*GL_RGBA32F*/ return spv::ImageFormat::Rgba32f;
|
||||
case 0x881A: /*GL_RGBA16F*/ return spv::ImageFormat::Rgba16f;
|
||||
case 0x8230: /*GL_RG32F*/ return spv::ImageFormat::Rg32f;
|
||||
case 0x822F: /*GL_RG16F*/ return spv::ImageFormat::Rg16f;
|
||||
case 0x8C3A: /*GL_R11F_G11F_B10F*/ return spv::ImageFormat::R11fG11fB10f;
|
||||
case 0x822E: /*GL_R32F*/ return spv::ImageFormat::R32f;
|
||||
case 0x822D: /*GL_R16F*/ return spv::ImageFormat::R16f;
|
||||
case 0x8D70: /*GL_RGBA32UI*/ return spv::ImageFormat::Rgba32ui;
|
||||
case 0x8D76: /*GL_RGBA16UI*/ return spv::ImageFormat::Rgba16ui;
|
||||
case 0x8D7C: /*GL_RGBA8UI*/ return spv::ImageFormat::Rgba8ui;
|
||||
case 0x906F: /*GL_RGB10_A2UI*/ return spv::ImageFormat::Rgb10a2ui;
|
||||
case 0x823C: /*GL_RG32UI*/ return spv::ImageFormat::Rg32ui;
|
||||
case 0x823A: /*GL_RG16UI*/ return spv::ImageFormat::Rg16ui;
|
||||
case 0x8238: /*GL_RG8UI*/ return spv::ImageFormat::Rg8ui;
|
||||
case 0x8236: /*GL_R32UI*/ return spv::ImageFormat::R32ui;
|
||||
case 0x8234: /*GL_R16UI*/ return spv::ImageFormat::R16ui;
|
||||
case 0x8232: /*GL_R8UI*/ return spv::ImageFormat::R8ui;
|
||||
case 0x8D82: /*GL_RGBA32I*/ return spv::ImageFormat::Rgba32i;
|
||||
case 0x8D88: /*GL_RGBA16I*/ return spv::ImageFormat::Rgba16i;
|
||||
case 0x8D8E: /*GL_RGBA8I*/ return spv::ImageFormat::Rgba8i;
|
||||
case 0x823B: /*GL_RG32I*/ return spv::ImageFormat::Rg32i;
|
||||
case 0x8239: /*GL_RG16I*/ return spv::ImageFormat::Rg16i;
|
||||
case 0x8237: /*GL_RG8I*/ return spv::ImageFormat::Rg8i;
|
||||
case 0x8235: /*GL_R32I*/ return spv::ImageFormat::R32i;
|
||||
case 0x8233: /*GL_R16I*/ return spv::ImageFormat::R16i;
|
||||
case 0x8231: /*GL_R8I*/ return spv::ImageFormat::R8i;
|
||||
case 0x8058: /*GL_RGBA8*/ return spv::ImageFormat::Rgba8;
|
||||
case 0x805B: /*GL_RGBA16*/ return spv::ImageFormat::Rgba16;
|
||||
case 0x8059: /*GL_RGB10_A2*/ return spv::ImageFormat::Rgb10A2;
|
||||
case 0x822B: /*GL_RG8*/ return spv::ImageFormat::Rg8;
|
||||
case 0x822C: /*GL_RG16*/ return spv::ImageFormat::Rg16;
|
||||
case 0x8229: /*GL_R8*/ return spv::ImageFormat::R8;
|
||||
case 0x822A: /*GL_R16*/ return spv::ImageFormat::R16;
|
||||
case 0x8F97: /*GL_RGBA8_SNORM*/ return spv::ImageFormat::Rgba8Snorm;
|
||||
case 0x8F9B: /*GL_RGBA16_SNORM*/ return spv::ImageFormat::Rgba16Snorm;
|
||||
case 0x8F95: /*GL_RG8_SNORM*/ return spv::ImageFormat::Rg8Snorm;
|
||||
case 0x8F99: /*GL_RG16_SNORM*/ return spv::ImageFormat::Rg16Snorm;
|
||||
case 0x8F94: /*GL_R8_SNORM*/ return spv::ImageFormat::R8Snorm;
|
||||
case 0x8F98: /*GL_R16_SNORM*/ return spv::ImageFormat::R16Snorm;
|
||||
default:
|
||||
return spv::ImageFormat::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Uint GLInternalFormatOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
case spv::ImageFormat::Rgba32f: return 0x8814; // GL_RGBA32F
|
||||
case spv::ImageFormat::Rgba16f: return 0x881A; // GL_RGBA16F
|
||||
case spv::ImageFormat::Rgba8: return 0x8058; // GL_RGBA8
|
||||
case spv::ImageFormat::Rgba8Snorm: return 0x8F97; // GL_RGBA8_SNORM
|
||||
case spv::ImageFormat::Rgba32i: return 0x8D82; // GL_RGBA32I
|
||||
case spv::ImageFormat::Rgba16i: return 0x8D88; // GL_RGBA16I
|
||||
case spv::ImageFormat::Rgba8i: return 0x8D8E; // GL_RGBA8I
|
||||
case spv::ImageFormat::Rgba32ui: return 0x8D70; // GL_RGBA32UI
|
||||
case spv::ImageFormat::Rgba16ui: return 0x8D76; // GL_RGBA16UI
|
||||
case spv::ImageFormat::Rgba8ui: return 0x8D7C; // GL_RGBA8UI
|
||||
default:
|
||||
// Only the carriers need the reverse direction, and every carrier is one
|
||||
// of the four-channel core formats above.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ChannelsOfSpirvImageFormat(spv::ImageFormat format) {
|
||||
switch (format) {
|
||||
case spv::ImageFormat::R32f:
|
||||
case spv::ImageFormat::R16f:
|
||||
case spv::ImageFormat::R16:
|
||||
case spv::ImageFormat::R8:
|
||||
case spv::ImageFormat::R16Snorm:
|
||||
case spv::ImageFormat::R8Snorm:
|
||||
case spv::ImageFormat::R32i:
|
||||
case spv::ImageFormat::R16i:
|
||||
case spv::ImageFormat::R8i:
|
||||
case spv::ImageFormat::R32ui:
|
||||
case spv::ImageFormat::R16ui:
|
||||
case spv::ImageFormat::R8ui:
|
||||
return 1;
|
||||
case spv::ImageFormat::Rg32f:
|
||||
case spv::ImageFormat::Rg16f:
|
||||
case spv::ImageFormat::Rg16:
|
||||
case spv::ImageFormat::Rg8:
|
||||
case spv::ImageFormat::Rg16Snorm:
|
||||
case spv::ImageFormat::Rg8Snorm:
|
||||
case spv::ImageFormat::Rg32i:
|
||||
case spv::ImageFormat::Rg16i:
|
||||
case spv::ImageFormat::Rg8i:
|
||||
case spv::ImageFormat::Rg32ui:
|
||||
case spv::ImageFormat::Rg16ui:
|
||||
case spv::ImageFormat::Rg8ui:
|
||||
return 2;
|
||||
case spv::ImageFormat::R11fG11fB10f:
|
||||
return 3;
|
||||
case spv::ImageFormat::Rgba32f:
|
||||
case spv::ImageFormat::Rgba16f:
|
||||
case spv::ImageFormat::Rgba16:
|
||||
case spv::ImageFormat::Rgb10A2:
|
||||
case spv::ImageFormat::Rgba8:
|
||||
case spv::ImageFormat::Rgba16Snorm:
|
||||
case spv::ImageFormat::Rgba8Snorm:
|
||||
case spv::ImageFormat::Rgba32i:
|
||||
case spv::ImageFormat::Rgba16i:
|
||||
case spv::ImageFormat::Rgba8i:
|
||||
case spv::ImageFormat::Rgba32ui:
|
||||
case spv::ImageFormat::Rgba16ui:
|
||||
case spv::ImageFormat::Rgba8ui:
|
||||
case spv::ImageFormat::Rgb10a2ui:
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsWidenableStorageImageType(const Instruction* type) {
|
||||
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));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint WidenImageFormatsPass::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
|
||||
const ImageFormatWidening widening =
|
||||
WideningOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
|
||||
if (!widening) return 0;
|
||||
return GLInternalFormatOfSpirvImageFormat(widening.Carrier);
|
||||
}
|
||||
|
||||
Uint WidenImageFormatsPass::ImageFormatChannelCount(Uint glInternalFormat) {
|
||||
return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
|
||||
}
|
||||
|
||||
bool WidenImageFormatsPass::DeclaresWidenableImageFormat(const Vector<Uint32>& binary) {
|
||||
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());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
for (const Instruction& type : context->module()->types_values()) {
|
||||
if (IsWidenableStorageImageType(&type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status WidenImageFormatsPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// Cheap gate first: no widenable image type, and the module is handed back
|
||||
// byte-identical - which is every shader but a handful.
|
||||
std::vector<Instruction*> imageTypes;
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (IsWidenableStorageImageType(&type)) {
|
||||
imageTypes.push_back(&type);
|
||||
}
|
||||
}
|
||||
if (imageTypes.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// What each widenable image type becomes, and the mask its accesses take. Keyed on
|
||||
// the type's result id so the access walk below can ask about an image VALUE by
|
||||
// its type without re-deriving anything.
|
||||
struct WidenedImage {
|
||||
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
|
||||
uint32_t Channels = 0;
|
||||
uint32_t SampledTypeId = 0;
|
||||
};
|
||||
std::map<uint32_t, WidenedImage> widenedByTypeId;
|
||||
for (Instruction* type : imageTypes) {
|
||||
const auto format =
|
||||
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
|
||||
const ImageFormatWidening widening = WideningOfSpirvImageFormat(format);
|
||||
widenedByTypeId.emplace(type->result_id(),
|
||||
WidenedImage{widening.Carrier, widening.Channels,
|
||||
type->GetSingleWordInOperand(kImageSampledTypeOperand)});
|
||||
}
|
||||
|
||||
// Collect the accesses BEFORE anything is mutated, and refuse the whole rewrite if
|
||||
// any of them is a shape this pass cannot mask end to end. A widened declaration
|
||||
// whose accesses were left unmasked is worse than the compile error it replaced:
|
||||
// the shader runs and quietly reads the carrier's surplus channels, which GL says
|
||||
// are 0 and 1. Refusing hands the stage back to the "no GLSL ES spelling"
|
||||
// diagnostic instead, which at least names the failure.
|
||||
std::vector<Instruction*> reads;
|
||||
std::vector<Instruction*> writes;
|
||||
Bool rewritable = true;
|
||||
for (auto funcIt = irContext->module()->begin();
|
||||
funcIt != irContext->module()->end() && rewritable; ++funcIt) {
|
||||
funcIt->ForEachInst([&](Instruction* inst) {
|
||||
if (!rewritable) return;
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpImageRead:
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageSparseRead:
|
||||
case spv::Op::OpImageTexelPointer:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// OpImageTexelPointer names the image VARIABLE (a pointer), the other
|
||||
// three an image VALUE; both reach the OpTypeImage through the def's
|
||||
// type, one hop further for the pointer.
|
||||
const Instruction* imageDef =
|
||||
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
||||
if (imageDef == nullptr) return;
|
||||
uint32_t imageTypeId = imageDef->type_id();
|
||||
if (const Instruction* imageType = defUseMgr->GetDef(imageTypeId);
|
||||
imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) {
|
||||
imageTypeId = imageType->GetSingleWordInOperand(1);
|
||||
}
|
||||
const auto widenedIt = widenedByTypeId.find(imageTypeId);
|
||||
if (widenedIt == widenedByTypeId.end()) return;
|
||||
|
||||
if (inst->opcode() == spv::Op::OpImageRead) {
|
||||
reads.push_back(inst);
|
||||
return;
|
||||
}
|
||||
if (inst->opcode() == spv::Op::OpImageWrite) {
|
||||
writes.push_back(inst);
|
||||
return;
|
||||
}
|
||||
// OpImageSparseRead yields a struct rather than a plain texel vector, and
|
||||
// OpImageTexelPointer is an image atomic - which spirv-val already
|
||||
// restricts to r32i/r32ui/r32f, all three of them core formats that never
|
||||
// reach this table. Neither is expressible in the ESSL this backend emits,
|
||||
// so rather than mask a shape that has never been seen, decline.
|
||||
rewritable = false;
|
||||
});
|
||||
}
|
||||
if (!rewritable) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// The four-component (0, .., 0, 1) constant each mask shuffles its surplus
|
||||
// channels out of, one per component type in play. GL defines an imageLoad from a
|
||||
// format with fewer than four channels as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so pinning the carrier's
|
||||
// surplus channels to exactly these values is the whole of the emulation.
|
||||
std::map<uint32_t, uint32_t> zeroOneConstantBySampledType; // sampled type id -> constant id
|
||||
std::map<uint32_t, uint32_t> vec4TypeBySampledType; // sampled type id -> v4 type id
|
||||
auto resolveMaskMaterial = [&](uint32_t sampledTypeId, uint32_t& outConstantId,
|
||||
uint32_t& outVec4TypeId) -> Bool {
|
||||
if (const auto cached = zeroOneConstantBySampledType.find(sampledTypeId);
|
||||
cached != zeroOneConstantBySampledType.end()) {
|
||||
outConstantId = cached->second;
|
||||
outVec4TypeId = vec4TypeBySampledType[sampledTypeId];
|
||||
return outConstantId != 0 && outVec4TypeId != 0;
|
||||
}
|
||||
const Instruction* sampledType = defUseMgr->GetDef(sampledTypeId);
|
||||
if (sampledType == nullptr) return false;
|
||||
|
||||
uint32_t oneWord = 0;
|
||||
std::unique_ptr<analysis::Type> component;
|
||||
if (sampledType->opcode() == spv::Op::OpTypeFloat &&
|
||||
sampledType->GetSingleWordInOperand(0) == 32) {
|
||||
component = spvtools::MakeUnique<analysis::Float>(32);
|
||||
oneWord = 0x3F800000u; // 1.0f
|
||||
} else if (sampledType->opcode() == spv::Op::OpTypeInt &&
|
||||
sampledType->GetSingleWordInOperand(0) == 32) {
|
||||
// OpTypeInt in-operands: 0 width, 1 signedness.
|
||||
component = spvtools::MakeUnique<analysis::Integer>(
|
||||
32, sampledType->GetSingleWordInOperand(1) != 0);
|
||||
oneWord = 1u;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
analysis::Type* componentReg = typeMgr->GetRegisteredType(component.get());
|
||||
if (componentReg == nullptr) return false;
|
||||
const analysis::Constant* zero = constantMgr->GetConstant(componentReg, {0u});
|
||||
const analysis::Constant* one = constantMgr->GetConstant(componentReg, {oneWord});
|
||||
if (zero == nullptr || one == nullptr) return false;
|
||||
const Instruction* zeroInst = constantMgr->GetDefiningInstruction(zero);
|
||||
const Instruction* oneInst = constantMgr->GetDefiningInstruction(one);
|
||||
if (zeroInst == nullptr || oneInst == nullptr) return false;
|
||||
|
||||
analysis::Vector vector(componentReg, 4);
|
||||
const uint32_t vec4TypeId = typeMgr->GetTypeInstruction(&vector);
|
||||
if (vec4TypeId == 0) return false;
|
||||
// Through the id rather than through GetRegisteredType(&vector): the
|
||||
// instruction the line above declared (or found) is the one the constant has
|
||||
// to be typed by, and asking the manager for its type is what guarantees the
|
||||
// two are the same registered object.
|
||||
analysis::Type* vectorReg = typeMgr->GetType(vec4TypeId);
|
||||
if (vectorReg == nullptr) return false;
|
||||
// A vector constant's "literal words" are the IDS of its components
|
||||
// (ConstantManager::CreateConstant -> GetConstantsFromIds).
|
||||
const analysis::Constant* zeroOne = constantMgr->GetConstant(
|
||||
vectorReg, {zeroInst->result_id(), zeroInst->result_id(), zeroInst->result_id(),
|
||||
oneInst->result_id()});
|
||||
if (zeroOne == nullptr) return false;
|
||||
const Instruction* zeroOneInst = constantMgr->GetDefiningInstruction(zeroOne);
|
||||
if (zeroOneInst == nullptr) return false;
|
||||
|
||||
outConstantId = zeroOneInst->result_id();
|
||||
outVec4TypeId = vec4TypeId;
|
||||
zeroOneConstantBySampledType.emplace(sampledTypeId, outConstantId);
|
||||
vec4TypeBySampledType.emplace(sampledTypeId, outVec4TypeId);
|
||||
return true;
|
||||
};
|
||||
|
||||
// OpVectorShuffle selects components 0-3 from the first vector and 4-7 from the
|
||||
// second, so with (0, 0, 0, 1) as the second operand the mask for a `channels`-
|
||||
// channel format is [0 .. channels-1] followed by 4 + i for the rest: the surplus
|
||||
// channels take the constant's 0s and, at index 3, its 1.
|
||||
auto maskComponents = [](uint32_t channels) {
|
||||
std::vector<Operand> components;
|
||||
components.reserve(4);
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
components.push_back(
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {i < channels ? i : 4u + i}});
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
||||
auto widenedOf = [&](const Instruction* inst) -> const WidenedImage* {
|
||||
const Instruction* imageDef =
|
||||
defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand));
|
||||
if (imageDef == nullptr) return nullptr;
|
||||
const auto it = widenedByTypeId.find(imageDef->type_id());
|
||||
return it == widenedByTypeId.end() ? nullptr : &it->second;
|
||||
};
|
||||
|
||||
// Every constant and vector type the masks will need, declared BEFORE the first
|
||||
// instruction is inserted. The constant and type managers append to the module's
|
||||
// globals and keep their own def-use bookkeeping straight; the shuffles below do
|
||||
// not (this pass invalidates every analysis at the end instead), so doing the two
|
||||
// in the other order would have the managers consult a def-use map that no longer
|
||||
// describes the function bodies.
|
||||
for (const auto& widened : widenedByTypeId) {
|
||||
uint32_t unusedConstantId = 0;
|
||||
uint32_t unusedVec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened.second.SampledTypeId, unusedConstantId, unusedVec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
}
|
||||
|
||||
// Masks first, while every image type still carries its ORIGINAL format: the
|
||||
// rewrite below only touches the format operand, so the accesses' types do not
|
||||
// move and the order is free either way - but doing it first keeps a failed
|
||||
// resolve from leaving a half-widened module behind.
|
||||
for (Instruction* write : writes) {
|
||||
const WidenedImage* widened = widenedOf(write);
|
||||
if (widened == nullptr) continue;
|
||||
uint32_t zeroOneId = 0;
|
||||
uint32_t vec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t texelId = write->GetSingleWordInOperand(kImageWriteTexelOperand);
|
||||
const Instruction* texel = defUseMgr->GetDef(texelId);
|
||||
// SPIR-V allows a scalar texel; GLSL's imageStore always passes a gvec4, and a
|
||||
// shape this has never seen is refused rather than guessed at.
|
||||
if (texel == nullptr || texel->type_id() != vec4TypeId) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t maskedId = irContext->TakeNextId();
|
||||
if (maskedId == 0) return Status::Failure;
|
||||
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
|
||||
for (const Operand& component : maskComponents(widened->Channels)) {
|
||||
shuffleOperands.push_back(component);
|
||||
}
|
||||
write->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands));
|
||||
write->SetInOperand(kImageWriteTexelOperand, {maskedId});
|
||||
}
|
||||
|
||||
for (Instruction* read : reads) {
|
||||
const WidenedImage* widened = widenedOf(read);
|
||||
if (widened == nullptr) continue;
|
||||
uint32_t zeroOneId = 0;
|
||||
uint32_t vec4TypeId = 0;
|
||||
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
if (read->type_id() != vec4TypeId) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
// The ORIGINAL instruction keeps its result id and becomes the shuffle, and a
|
||||
// copy of the read is inserted in front of it under a fresh id. That way every
|
||||
// existing use of the read stays intact without a ReplaceAllUsesWith that
|
||||
// would also rewrite the shuffle's own operand (the idiom
|
||||
// EmulateNoPerspectivePass uses for the same reason).
|
||||
const uint32_t rawReadId = irContext->TakeNextId();
|
||||
if (rawReadId == 0) return Status::Failure;
|
||||
Instruction::OperandList readOperands;
|
||||
for (uint32_t i = 0; i < read->NumInOperands(); ++i) {
|
||||
readOperands.push_back(read->GetInOperand(i));
|
||||
}
|
||||
read->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpImageRead, vec4TypeId, rawReadId, readOperands));
|
||||
read->SetOpcode(spv::Op::OpVectorShuffle);
|
||||
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {rawReadId}},
|
||||
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
|
||||
for (const Operand& component : maskComponents(widened->Channels)) {
|
||||
shuffleOperands.push_back(component);
|
||||
}
|
||||
read->SetInOperands(Move(shuffleOperands));
|
||||
}
|
||||
|
||||
// The declaration itself, last. Only the format operand moves: the carrier has the
|
||||
// same component type as the original by construction, so the OpTypeImage's
|
||||
// Sampled Type still agrees with it (which is what spirv-val checks) and no
|
||||
// pointer, array or access-chain type has to be rebuilt.
|
||||
//
|
||||
// Two image types can COLLIDE here - `layout(rg32f)` and `layout(rgba32f)` in one
|
||||
// module both become Rgba32f - and duplicate non-aggregate type declarations are
|
||||
// invalid SPIR-V. The caller runs spirv-tools' RemoveDuplicates pass immediately
|
||||
// after this one, which joins them (and cascades to the pointer and array types
|
||||
// that named them) rather than this pass carrying its own join.
|
||||
for (Instruction* type : imageTypes) {
|
||||
const auto widenedIt = widenedByTypeId.find(type->result_id());
|
||||
if (widenedIt == widenedByTypeId.end()) continue;
|
||||
type->SetInOperand(kImageFormatOperand, {static_cast<uint32_t>(widenedIt->second.Carrier)});
|
||||
defUseMgr->AnalyzeInstUse(type);
|
||||
}
|
||||
|
||||
// StorageImageExtendedFormats is deliberately left declared even though every
|
||||
// remaining format is now one of the thirteen that need no capability: a
|
||||
// capability a module no longer exercises is valid SPIR-V, and dropping one is
|
||||
// only safe after proving no extended format is left ANYWHERE, including in image
|
||||
// types this pass declined.
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken WidenImageFormatsPass::CreateWidenImageFormatsPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<WidenImageFormatsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,101 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h
|
||||
// Copyright (c) 2025-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 "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Emulates the desktop-GL image formats GLSL ES cannot spell by CHANNEL WIDENING: a
|
||||
// storage image DECLARED `layout(rg32f)` is re-declared `layout(rgba32f)` and every
|
||||
// access through it is masked back to the two channels GL says it has.
|
||||
//
|
||||
// WHY IT IS NEEDED AT ALL. GL 4.2 has forty image formats; GLSL ES 3.1 has thirteen,
|
||||
// and GL_NV_image_formats - the only extension that adds the rest - is advertised by
|
||||
// none of Adreno 830, Mali-G1-Ultra MC12 or Mali-G925-Immortalis MC12 (probed on all
|
||||
// three, with `#extension ... : enable` also rejected, so "the driver implements it
|
||||
// unadvertised" is refuted rather than assumed). A shader that declares one of the
|
||||
// other twenty-six therefore has NO legal ESSL spelling, and it fails in one of two
|
||||
// ways: SPIRV-Cross throws for its is_desktop_only_format set and no text is produced
|
||||
// at all, or the token reaches the driver and is rejected ("'rg32f' : not a legal
|
||||
// layout qualifier id"). Either way the stage is lost, the backend program is
|
||||
// unusable, and every draw with it silently renders nothing while the frontend keeps
|
||||
// reporting GL_LINK_STATUS = TRUE. Dropping the qualifier instead is not an escape:
|
||||
// all three drivers reject a format-LESS image declaration outright ("all images have
|
||||
// to define layout format" / "S0001: Image must specify a format layout qualifier"),
|
||||
// readonly and writeonly alike, at both #version 310 es and 320 es. And unlike a
|
||||
// numeric limit there is nothing honest to report either - GL has no "this image
|
||||
// format is unsupported" query - so the format has to be emulated.
|
||||
//
|
||||
// WHAT WIDENING MEANS. Seventeen of the twenty-six have a core ESSL format of the
|
||||
// SAME PER-CHANNEL WIDTH AND COMPONENT TYPE, differing only in channel count
|
||||
// (rg32f -> rgba32f, r8ui -> rgba8ui, rg8_snorm -> rgba8_snorm, ...). Carried in one
|
||||
// of those the emulation is EXACT, not approximate: every value is representable bit
|
||||
// for bit, and GL's own image semantics do the rest -
|
||||
//
|
||||
// * imageLoad on a format with fewer than four channels returns (r, 0, 0, 1);
|
||||
// * imageStore drops the components the format does not have.
|
||||
//
|
||||
// so the two surplus channels of the carrier are not free storage, they are values GL
|
||||
// already defines. This pass pins them: every OpImageWrite through a widened image has
|
||||
// its texel replaced by (r[, g[, b]], 0.., 1) and every OpImageRead has its result
|
||||
// masked the same way. Masking BOTH is deliberate belt and braces - the write mask
|
||||
// alone keeps the storage canonical for a sampler and for glGetTexImage, the read mask
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// glBindImageTexture has to be handed the carrier (on Adreno the bind of the narrow
|
||||
// format is GL_INVALID_VALUE for nineteen of the twenty-six, and on both Malis for
|
||||
// twenty-five). Both are done in DirectGLES against the same table below, so the two
|
||||
// sides agree by construction rather than by convention. Binding a narrow texture
|
||||
// through a wide image is NOT an option: every tested driver accepts it silently, so
|
||||
// it reads and writes out of bounds undetected.
|
||||
//
|
||||
// ESSL ONLY. DirectVulkan takes the declared format natively and resolves the view
|
||||
// format from the same bind state, so the module must reach it unchanged.
|
||||
class WidenImageFormatsPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
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);
|
||||
|
||||
// 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
|
||||
// exactly (the nine above, and anything that is not an image format at all).
|
||||
// Used by DirectGLES for the texture storage and the glBindImageTexture argument,
|
||||
// so that all three layers pick the same carrier.
|
||||
static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat);
|
||||
|
||||
// Channels the GL internal format really has (1-4), or 0 when it is not one of the
|
||||
// forty image formats. The count the widened accesses are masked back to.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -162,6 +162,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
|
||||
builder.Value(inputs.maxColorTextureSamples);
|
||||
builder.Value(inputs.maxIntegerSamples);
|
||||
builder.Value(inputs.maxDepthTextureSamples);
|
||||
|
||||
@@ -602,6 +602,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// --- driver capability bits that arm or steer a pass ---
|
||||
Bool supportsViewportArray = 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
|
||||
// masks its accesses back - so a driver that HAS the extension and one that does not get
|
||||
// materially different ESSL from the same module.
|
||||
Bool supportsExtendedImageFormats = false;
|
||||
Int32 maxColorTextureSamples = 0;
|
||||
Int32 maxIntegerSamples = 0;
|
||||
Int32 maxDepthTextureSamples = 0;
|
||||
|
||||
Reference in New Issue
Block a user