mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Feat] (MG_Util, DirectGLES, DirectVulkan): normalize rectangle coordinates in the module
Neither target API has GL_TEXTURE_RECTANGLE: ESSL has no rectangle sampler, and Vulkan's SPIR-V environment does not allow Dim::Rect. Both emulate it on a plain 2D texture, and the two differ in exactly one way - a rectangle lookup addresses texels where a 2D one addresses [0,1]. That one difference now lives in one SPIR-V pass, so neither backend has to know about it: every lookup taking normalized coordinates gets its coordinate divided by the size the texture reports, and the image type is then rewritten to 2D. Magma had no rectangle handling at all - it fed Dim::Rect straight to Vulkan, which read the texel coordinates as normalized and sampled the edge, so all fifteen KHR-GL40.texture_gather.*-2drect cases came back holding the clear colour. This replaces the ESSL text rewrite that did the same divide for DirectGLES only. Doing it in the module instead is both shorter and stricter: the pass resolves an operation's image type through the sampled-image and pointer wrappers rather than matching a sampler name in generated source, so it cannot be fooled by an expression where it expected an identifier, and it needs no help from the frontend reflection to know which samplers were rectangles. Still declined, as before: the Dref *sample* forms, whose coordinate carries the compare value in its last component, and the projective ones, where the divide would have to happen after the perspective divide. texelFetch is deliberately untouched - integer texel coordinates mean the same thing on both targets. KHR-GL40.texture_gather: Magma 66 failures -> 2, Espryt stays at 75/75.
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -363,90 +364,16 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ...
|
||||
constexpr SizeT kTypeImageDimWordIndex = 3;
|
||||
constexpr SizeT kTypeImageMinWordCount = 9;
|
||||
outputBinary.clear();
|
||||
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||
return false;
|
||||
}
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Vector<SizeT> rectDimWordOffsets;
|
||||
Vector<SizeT> rectCapabilityWordOffsets;
|
||||
Bool hasNormalizedCoordinateLookup = false;
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||
const Uint32 instructionWord = inputBinary[offset];
|
||||
const SizeT wordCount = instructionWord >> 16u;
|
||||
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
|
||||
return false;
|
||||
}
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
|
||||
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) {
|
||||
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
|
||||
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
|
||||
}
|
||||
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
|
||||
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
|
||||
if (capability == spv::Capability::SampledRect ||
|
||||
capability == spv::Capability::ImageRect) {
|
||||
rectCapabilityWordOffsets.push_back(offset + 1);
|
||||
}
|
||||
} else {
|
||||
switch (opcode) {
|
||||
// Normalized-coordinate lookups whose ESSL form the backend's
|
||||
// NormalizeRectSamplerCoordinates post-pass cannot repair: the
|
||||
// coordinate is either fused with something else in a single argument
|
||||
// (the Dref sample forms carry the compare value in coord.z) or the
|
||||
// divide would have to happen after a projective divide. Tracing each
|
||||
// one back to its image type would let a module mix a normalized 2D
|
||||
// lookup with a rectangle fetch, but the extra reach is not worth the
|
||||
// risk of getting the trace wrong: decline the whole module instead.
|
||||
//
|
||||
// OpImageSampleImplicitLod, OpImageGather and OpImageDrefGather are
|
||||
// absent because all three become an ESSL call whose argument 1 is the
|
||||
// bare texel-space coordinate, which the post-pass divides by the
|
||||
// texture size.
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
hasNormalizedCoordinateLookup = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outputBinary.assign(inputBinary.begin(), inputBinary.end());
|
||||
for (const SizeT dimWordOffset : rectDimWordOffsets) {
|
||||
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is always
|
||||
// declared by a graphics module, so restating it keeps the word count intact
|
||||
// without leaving a capability SPIRV-Cross would key off.
|
||||
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
|
||||
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
|
||||
}
|
||||
return true;
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
|
||||
@@ -43,20 +43,15 @@ namespace MobileGL {
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
|
||||
// for them at all - it refuses outright ("Rectangle textures are not supported on
|
||||
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
|
||||
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
|
||||
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
|
||||
// lookup would also need its coordinates divided by the texture size, so the pass
|
||||
// declines those modules instead of emitting something subtly wrong. Returns false
|
||||
// when it changed nothing or cannot safely convert. DirectGLES only.
|
||||
static bool LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
// which wrongly includes baseInstance).
|
||||
// GL_TEXTURE_RECTANGLE emulated on a plain 2D texture, for every backend:
|
||||
// divides the coordinate of each normalized-coordinate lookup by the texture
|
||||
// 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);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Adds the Invariant decoration to every Position builtin output. GL apps
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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 "NormalizeRectCoordinatesPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#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 <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
|
||||
// Image operations whose coordinate operand (in-operand 1) is a plain
|
||||
// normalized coordinate and nothing else. The Dref *sample* forms are absent:
|
||||
// they pack the compare value into the coordinate's last component, so the
|
||||
// divide cannot be applied componentwise. OpImageDrefGather is here because it
|
||||
// carries the compare value in a separate operand.
|
||||
bool TakesPlainNormalizedCoordinate(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a sampled
|
||||
// image, a bare image, or a pointer to either. Returns nullptr when the operand
|
||||
// is not an image at all.
|
||||
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:
|
||||
// Both name their element type in their last in-operand.
|
||||
type = defUseMgr->GetDef(type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
||||
continue;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsRectImageType(const Instruction* imageType) {
|
||||
// OpTypeImage in-operands: sampled type, Dim, Depth, Arrayed, MS, Sampled, Format.
|
||||
return imageType != nullptr && imageType->NumInOperands() >= 2 &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) == spv::Dim::Rect;
|
||||
}
|
||||
|
||||
// The bare image an OpImageQuerySizeLod needs. An operation on a sampled image
|
||||
// has to unwrap it first; one already holding a bare image is used as is.
|
||||
uint32_t GetQueryableImage(IRContext* context, InstructionBuilder& builder, uint32_t imageOperandId,
|
||||
uint32_t imageTypeId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(imageOperandId);
|
||||
if (object == nullptr) return 0;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
if (type != nullptr && type->opcode() == spv::Op::OpTypeImage) {
|
||||
return imageOperandId;
|
||||
}
|
||||
Instruction* unwrapped = builder.AddUnaryOp(imageTypeId, spv::Op::OpImage, imageOperandId);
|
||||
return unwrapped != nullptr ? unwrapped->result_id() : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status NormalizeRectCoordinatesPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
// Nothing to do unless the module actually declares a rectangle image.
|
||||
bool hasRectImageType = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
|
||||
hasRectImageType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasRectImageType) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::opt::analysis::Integer signedInt(32, true);
|
||||
spvtools::opt::analysis::Float float32(32);
|
||||
spvtools::opt::analysis::Vector int2(&signedInt, 2);
|
||||
spvtools::opt::analysis::Vector float2(&float32, 2);
|
||||
const uint32_t int2TypeId = typeMgr->GetTypeInstruction(&int2);
|
||||
const uint32_t float2TypeId = typeMgr->GetTypeInstruction(&float2);
|
||||
const uint32_t lodZeroId = constantMgr->GetSIntConstId(0);
|
||||
if (int2TypeId == 0 || float2TypeId == 0 || lodZeroId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
bool rewroteCoordinate = false;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (!TakesPlainNormalizedCoordinate(instruction.opcode()) ||
|
||||
instruction.NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t imageOperandId = instruction.GetSingleWordInOperand(0);
|
||||
Instruction* imageType = ResolveImageType(irContext, imageOperandId);
|
||||
if (!IsRectImageType(imageType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t coordinateId = instruction.GetSingleWordInOperand(1);
|
||||
InstructionBuilder builder(
|
||||
irContext, &instruction,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
const uint32_t queryableImageId =
|
||||
GetQueryableImage(irContext, builder, imageOperandId, imageType->result_id());
|
||||
if (queryableImageId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
// A rectangle image has exactly one level, so the query's level is 0.
|
||||
// The lod form is what the 2D type this becomes accepts.
|
||||
Instruction* size = builder.AddBinaryOp(int2TypeId, spv::Op::OpImageQuerySizeLod,
|
||||
queryableImageId, lodZeroId);
|
||||
Instruction* sizeFloat =
|
||||
builder.AddUnaryOp(float2TypeId, spv::Op::OpConvertSToF, size->result_id());
|
||||
Instruction* normalized = builder.AddBinaryOp(
|
||||
float2TypeId, spv::Op::OpFDiv, coordinateId, sizeFloat->result_id());
|
||||
instruction.SetInOperand(1, {normalized->result_id()});
|
||||
irContext->UpdateDefUse(&instruction);
|
||||
rewroteCoordinate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that no lookup depends on the rectangle semantics any more, the type can
|
||||
// become the 2D one both targets accept. Done unconditionally, because a module
|
||||
// that only ever fetched texels still has to lose the type.
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
|
||||
type.SetInOperand(1, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is
|
||||
// always declared by a graphics module, so restating it keeps the instruction
|
||||
// valid without leaving a capability a consumer would key off.
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
const auto value = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (value == spv::Capability::SampledRect || value == spv::Capability::ImageRect) {
|
||||
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
|
||||
}
|
||||
}
|
||||
if (rewroteCoordinate) {
|
||||
irContext->AddCapability(spv::Capability::ImageQuery);
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<NormalizeRectCoordinatesPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,38 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL_TEXTURE_RECTANGLE has no counterpart in either target API: ESSL has no
|
||||
// rectangle sampler at all, and Vulkan's SPIR-V environment does not allow
|
||||
// Dim::Rect. Both emulate it on a plain 2D texture, which differs in exactly one
|
||||
// way - a rectangle lookup addresses texels while a 2D one addresses [0,1].
|
||||
//
|
||||
// This pass closes that difference in the module itself, so neither backend has
|
||||
// to reason about it: every lookup that takes normalized coordinates gets its
|
||||
// coordinate divided by the texture's size, and the image type is then rewritten
|
||||
// to 2D. texelFetch is untouched (integer texel coordinates mean the same thing
|
||||
// on both), and so is the Dref *sample* form, whose coordinate carries the
|
||||
// compare value in the last component - the module keeps its rectangle type and
|
||||
// the caller declines it.
|
||||
class NormalizeRectCoordinatesPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-normalize-rect-coordinates"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateNormalizeRectCoordinatesPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user