mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
300 lines
16 KiB
C++
300 lines
16 KiB
C++
// 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
|
|
|
|
Lower1DArrayImagesPass::ModuleTraits Lower1DArrayImagesPass::InspectBinary(const Vector<Uint32>& binary) {
|
|
ModuleTraits traits{};
|
|
if (binary.empty()) {
|
|
return traits;
|
|
}
|
|
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 traits;
|
|
}
|
|
|
|
// The type table settles it for the cheap half, and it is the half almost every
|
|
// shader takes: no such type declared, nothing to inspect further.
|
|
for (const Instruction& type : context->module()->types_values()) {
|
|
if (Is1DArrayStorageImageType(&type)) {
|
|
traits.declaresImage = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!traits.declaresImage) {
|
|
return traits;
|
|
}
|
|
|
|
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)))) {
|
|
traits.queriesImageSize = true;
|
|
return traits;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return traits;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// The same refusal the caller makes, restated here so the pass is safe wherever
|
|
// it is registered. Rewriting the type while leaving an OpImageQuerySize on it
|
|
// produces a query whose result type has one component too few - an invalid
|
|
// module - and there is no correct two-component size to substitute, because the
|
|
// ES texture genuinely has a height the GL one does not.
|
|
for (auto& function : *irContext->module()) {
|
|
for (auto& block : function) {
|
|
for (auto& instruction : block) {
|
|
if (QueriesImageSize(instruction.opcode()) && instruction.NumInOperands() >= 1 &&
|
|
Is1DArrayStorageImageType(
|
|
ResolveImageType(irContext, instruction.GetSingleWordInOperand(0)))) {
|
|
return Status::SuccessWithoutChange;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// (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);
|
|
|
|
// Built from the COORDINATE's own component type rather than a
|
|
// hardcoded signed int. GLSL only ever spells these ivec2, but SPIR-V
|
|
// permits an unsigned coordinate, and extracting a uint component
|
|
// into an int result is an invalid module rather than a wrong answer -
|
|
// the kind of defect that reaches a driver as "compiles here, not
|
|
// there".
|
|
Instruction* coordinateDef = irContext->get_def_use_mgr()->GetDef(coordinateId);
|
|
if (coordinateDef == nullptr) return Status::Failure;
|
|
const auto* coordinateType = typeMgr->GetType(coordinateDef->type_id());
|
|
const auto* coordinateVector = coordinateType != nullptr ? coordinateType->AsVector()
|
|
: nullptr;
|
|
if (coordinateVector == nullptr || coordinateVector->element_count() != 2) {
|
|
return Status::Failure;
|
|
}
|
|
const auto* component = coordinateVector->element_type();
|
|
const auto* componentInteger = component != nullptr ? component->AsInteger() : nullptr;
|
|
if (componentInteger == nullptr) return Status::Failure;
|
|
|
|
spvtools::opt::analysis::Vector widenedVector(component, 3);
|
|
const uint32_t int3TypeId = typeMgr->GetTypeInstruction(&widenedVector);
|
|
const uint32_t intTypeId = typeMgr->GetTypeInstruction(component);
|
|
const uint32_t zeroId = componentInteger->IsSigned()
|
|
? constantMgr->GetSIntConstId(0)
|
|
: constantMgr->GetUIntConstId(0);
|
|
if (int3TypeId == 0 || intTypeId == 0 || zeroId == 0) {
|
|
return Status::Failure;
|
|
}
|
|
|
|
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
|