mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
@@ -824,6 +825,34 @@ namespace MobileGL {
|
||||
return RunOptimizerChecked("LowerRectImages", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
// Declined rather than half-translated: after the rewrite the image is a 2D
|
||||
// array, so a size query on it yields three components where the shader consumes
|
||||
// two. Handing back a differently-shaped size silently is worse than leaving the
|
||||
// module alone and letting the driver say what it does not like - and unlike the
|
||||
// access path there is no correct answer to substitute, because the ES texture
|
||||
// genuinely has a height the GL one does not.
|
||||
//
|
||||
// MGLOG_I, deliberately: MGLOG_E/W are compiled out at the INFO level every CI,
|
||||
// retrace and release build uses, and this is exactly the diagnostic that has to
|
||||
// survive to explain the shader the driver is about to reject.
|
||||
if (Lower1DArrayImagesPass::BinaryQueriesA1DArrayStorageImageSize(inputBinary)) {
|
||||
MGLOG_I("[spirv] Lower1DArrayImagesForEssl: the module queries the size of a 1D-array "
|
||||
"storage image, which cannot be answered in the 2D-array shape ES stores it in; "
|
||||
"leaving the module alone, and a strict ES driver will reject it");
|
||||
outputBinary = inputBinary;
|
||||
return true;
|
||||
}
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass());
|
||||
|
||||
return RunOptimizerChecked("Lower1DArrayImagesForEssl", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -89,6 +89,14 @@ namespace MobileGL {
|
||||
// size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
|
||||
// what it declines and why.
|
||||
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
// GL_TEXTURE_1D_ARRAY storage images rewritten to the 2D-array shape the texture
|
||||
// is actually stored in on ES, with the layer moved from the coordinate's second
|
||||
// component to its third. DirectGLES transpile path only - Vulkan binds a real
|
||||
// VK_IMAGE_VIEW_TYPE_1D_ARRAY and must see the module unchanged. Copies the input
|
||||
// through untouched when the module declares no such image, which is every shader
|
||||
// but a handful. See Lower1DArrayImagesPass for what it declines and why.
|
||||
static bool Lower1DArrayImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Builds the non-indexed-draw variant of a vertex shader: every gl_BaseVertex
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.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 "Lower1DArrayImagesPass.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_builder.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 <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
|
||||
// 5 Sampled, 6 Format.
|
||||
constexpr uint32_t kDimOperand = 1;
|
||||
constexpr uint32_t kArrayedOperand = 3;
|
||||
constexpr uint32_t kSampledOperand = 5;
|
||||
|
||||
// A 1D image that is arrayed AND is a storage image. Sampled == 2 is SPIR-V's
|
||||
// "used without a sampler", i.e. exactly the image uniforms this pass exists for;
|
||||
// Sampled == 1 (a sampled image) reaches SPIRV-Cross's sampler path, which
|
||||
// already handles the 1D-array shape correctly and must be left to it.
|
||||
bool Is1DArrayStorageImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kSampledOperand &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) == spv::Dim::Dim1D &&
|
||||
imageType->GetSingleWordInOperand(kArrayedOperand) == 1u &&
|
||||
imageType->GetSingleWordInOperand(kSampledOperand) == 2u;
|
||||
}
|
||||
|
||||
// Any Dim1D image, sampled or storage. Used only to decide whether the Image1D
|
||||
// capability is still needed - deliberately wider than the rewrite's own
|
||||
// predicate, so a module that also holds a non-arrayed 1D image (which this pass
|
||||
// leaves to SPIRV-Cross) keeps the capability it still requires.
|
||||
bool IsDim1DImageType(const Instruction* imageType) {
|
||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
||||
imageType->NumInOperands() > kSampledOperand &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(kDimOperand)) == spv::Dim::Dim1D;
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a bare image,
|
||||
// or a pointer to one. Same unwrapping as NormalizeRectCoordinatesPass, minus the
|
||||
// sampled-image case a storage image never has.
|
||||
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(objectId);
|
||||
if (object == nullptr) return nullptr;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
while (type != nullptr) {
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeImage:
|
||||
return type;
|
||||
case spv::Op::OpTypeSampledImage:
|
||||
case spv::Op::OpTypePointer:
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
// Each names its element type in its last in-operand, except arrays,
|
||||
// whose element type is the FIRST. Both are reached here because an
|
||||
// image uniform may be declared as an array of images.
|
||||
type = defUseMgr->GetDef(type->opcode() == spv::Op::OpTypeArray ||
|
||||
type->opcode() == spv::Op::OpTypeRuntimeArray
|
||||
? type->GetSingleWordInOperand(0)
|
||||
: type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
||||
continue;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The coordinate operand index for the operations that address an image's texels.
|
||||
// OpImageRead and OpImageTexelPointer take (image, coordinate, ...); OpImageWrite
|
||||
// takes (image, coordinate, texel).
|
||||
bool TryGetCoordinateOperand(spv::Op opcode, uint32_t* coordinateOperand) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpImageRead:
|
||||
case spv::Op::OpImageSparseRead:
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpImageTexelPointer:
|
||||
*coordinateOperand = 1;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool QueriesImageSize(spv::Op opcode) {
|
||||
return opcode == spv::Op::OpImageQuerySize || opcode == spv::Op::OpImageQuerySizeLod ||
|
||||
opcode == spv::Op::OpImageQueryLevels || opcode == spv::Op::OpImageQuerySamples;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Lower1DArrayImagesPass::BinaryQueriesA1DArrayStorageImageSize(const Vector<Uint32>& binary) {
|
||||
if (binary.empty()) {
|
||||
return false;
|
||||
}
|
||||
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 (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (!QueriesImageSize(instruction.opcode()) || instruction.NumInOperands() < 1) {
|
||||
continue;
|
||||
}
|
||||
if (Is1DArrayStorageImageType(
|
||||
ResolveImageType(context.get(), instruction.GetSingleWordInOperand(0)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status Lower1DArrayImagesPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
// Nothing to do unless the module actually declares one. Every other shader pays
|
||||
// one walk of the type table and is handed back unchanged.
|
||||
bool hasType = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
hasType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasType) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::opt::analysis::Integer signedInt(32, true);
|
||||
spvtools::opt::analysis::Vector int3(&signedInt, 3);
|
||||
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&int3);
|
||||
const uint32_t intTypeId = typeMgr->GetTypeInstruction(&signedInt);
|
||||
const uint32_t zeroId = constantMgr->GetSIntConstId(0);
|
||||
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
// (u, layer) -> (u, 0, layer). The height the ES 2D array carries is 1, so Y is
|
||||
// always 0 and the layer has to move from the second component to the third; a
|
||||
// plain widening that appended the 0 would read layer 0 of every access instead.
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
uint32_t coordinateOperand = 0;
|
||||
if (!TryGetCoordinateOperand(instruction.opcode(), &coordinateOperand) ||
|
||||
instruction.NumInOperands() <= coordinateOperand) {
|
||||
continue;
|
||||
}
|
||||
if (!Is1DArrayStorageImageType(
|
||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t coordinateId = instruction.GetSingleWordInOperand(coordinateOperand);
|
||||
InstructionBuilder builder(
|
||||
irContext, &instruction,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
Instruction* u =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {0});
|
||||
Instruction* layer =
|
||||
builder.AddCompositeExtract(intTypeId, coordinateId, {1});
|
||||
if (u == nullptr || layer == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
Instruction* widened = builder.AddCompositeConstruct(
|
||||
int3TypeId, {u->result_id(), zeroId, layer->result_id()});
|
||||
if (widened == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
instruction.SetInOperand(coordinateOperand, {widened->result_id()});
|
||||
irContext->UpdateDefUse(&instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only now, with no access still spelling the 1D-array coordinate, does the type
|
||||
// become the 2D array one. Arrayed stays 1: this is a 2D ARRAY image, which is
|
||||
// what the texture was stored as.
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (Is1DArrayStorageImageType(&type)) {
|
||||
type.SetInOperand(kDimOperand, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
|
||||
// Image1D describes the types just rewritten - but only drop it if no 1D image
|
||||
// type is left at all. A module may hold a non-arrayed 1D storage image, which
|
||||
// this pass deliberately leaves to SPIRV-Cross, and that one still needs the
|
||||
// capability. Shader is always declared by any module reaching here, so restating
|
||||
// it keeps the instruction valid without leaving a capability a consumer could
|
||||
// key off.
|
||||
bool anyDim1DLeft = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (IsDim1DImageType(&type)) {
|
||||
anyDim1DLeft = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyDim1DLeft) {
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
const auto value = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (value == spv::Capability::Image1D) {
|
||||
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken Lower1DArrayImagesPass::CreateLower1DArrayImagesPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<Lower1DArrayImagesPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,78 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.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 {
|
||||
// ES has no 1D texture of any kind, so a GL_TEXTURE_1D_ARRAY is stored as an ES 2D
|
||||
// array with height 1 and the layers in depth (TextureImpl::MapToBackendTextureTarget
|
||||
// and GetBackendUploadSize, MG_Backend/DirectGLES/Managers.h). The shader side has to
|
||||
// agree, and for SAMPLERS it does: SPIRV-Cross rewrites a 1D-array lookup into a
|
||||
// 2D-array one and moves the layer into the third component itself
|
||||
// (spirv_glsl.cpp, `if (imgtype.image.arrayed) ... ".x, 0.0, " ... ".y"`).
|
||||
//
|
||||
// For IMAGES it does not. The image path applies the same 1D emulation without ever
|
||||
// asking whether the type is arrayed:
|
||||
//
|
||||
// if (type.image.dim == Dim1D && options.es)
|
||||
// coord_expr = join("ivec2(", coord_expr, ", 0)");
|
||||
//
|
||||
// For a non-arrayed 1D image that is right - a scalar coordinate becomes (u, 0). For
|
||||
// a 1D ARRAY image the coordinate is already the two-component (u, layer), so the
|
||||
// result is `ivec2(ivec2(u, layer), 0)`: three components crammed into a two-component
|
||||
// constructor. Every ES driver rejects it outright, and the whole program is lost -
|
||||
// which is how one uimage1DArray uniform took the entire eleven-image compute shader
|
||||
// of KHR-GL44.multi_bind.dispatch_bind_image_textures down with it, with the driver
|
||||
// saying only "'constructor' : too many arguments".
|
||||
//
|
||||
// Widening the constructor would not be enough either. `ivec3(u, layer, 0)` puts the
|
||||
// layer in the 2D array's Y and reads layer 0, whereas the storage this has to match
|
||||
// puts height at 1 and the layers in Z, so the correct coordinate is (u, 0, layer).
|
||||
//
|
||||
// So this pass does the whole conversion in the module, before SPIRV-Cross sees it:
|
||||
// every 1D-array STORAGE image type becomes a 2D-array one, and every read and write
|
||||
// through it has its coordinate widened from (u, layer) to (u, 0, layer). SPIRV-Cross
|
||||
// is then looking at an ordinary 2D array image and its 1D path never fires.
|
||||
//
|
||||
// Deliberately narrow, on three axes:
|
||||
//
|
||||
// * STORAGE images only (Sampled == 2). Sampled images reach SPIRV-Cross's sampler
|
||||
// path, which is correct today; rewriting them would replace working emission
|
||||
// with our own for no reason.
|
||||
// * ARRAYED only. A non-arrayed 1D storage image is emitted correctly by the same
|
||||
// SPIRV-Cross code, and is left to it.
|
||||
// * ESSL only. Vulkan has VK_IMAGE_VIEW_TYPE_1D_ARRAY natively and Magma binds it
|
||||
// directly, so the module must reach that backend unchanged.
|
||||
//
|
||||
// A size query on one of these images is DECLINED rather than half-translated: after
|
||||
// the rewrite OpImageQuerySize yields three components where the shader consumes two,
|
||||
// and silently handing back a differently-shaped size is worse than refusing. The
|
||||
// caller logs it and leaves the module alone.
|
||||
class Lower1DArrayImagesPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-lower-1d-array-images"; }
|
||||
Status Process() override;
|
||||
|
||||
// True when the module declares a 1D-array storage image whose size is queried,
|
||||
// which is the shape this pass refuses to translate. Checked by the caller before
|
||||
// running, so a declined module is handed on untouched rather than partly
|
||||
// rewritten.
|
||||
static bool BinaryQueriesA1DArrayStorageImageSize(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLower1DArrayImagesPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user