mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (MG_Util, MG_Backend/DirectVulkan): GL reads gl_BaseVertex as zero on a non-indexed draw where Vulkan's builtin hands over firstVertex
This commit is contained in:
@@ -279,6 +279,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
|
||||
@@ -1979,13 +1979,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
|
||||
// detects the case so the user gets one warning instead of silent corruption.
|
||||
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInInstanceIndex);
|
||||
}
|
||||
|
||||
// GL's gl_BaseVertex and Vulkan's BaseVertex agree for indexed draws and disagree for every
|
||||
// other command, so a program declaring the builtin needs the ZeroBaseVertex variant when a
|
||||
// non-indexed draw uses it (see CompileOptionBit::ZeroBaseVertex). "Declares" rather than
|
||||
// "reads" is the honest word and the useful one: the zeroing pass keeps the variable, so
|
||||
// both variants of a program answer this question identically.
|
||||
Bool ProgramFactory::ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == SpvBuiltInInstanceIndex) {
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2244,6 +2258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.activeVertexInputLocationMask = 0;
|
||||
entry.vertexInputTypes.fill(0);
|
||||
entry.readsBaseVertexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
||||
@@ -2265,6 +2280,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.readsBaseVertexBuiltin = ReflectedReadsBaseVertexBuiltin(reflectModule);
|
||||
|
||||
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||
if (!s_warnedInstanceIndexUnsupported) {
|
||||
@@ -2909,6 +2926,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
|
||||
// that height rides in the free high half of the key. Flags occupy the low bits, and a
|
||||
// height cannot exceed the 16 bits a swapchain extent fits in.
|
||||
//
|
||||
// "The low bits" is load-bearing and was until now only a comment: a flag that reached
|
||||
// bit 16 would alias the height and two different variants would share one memo slot.
|
||||
static_assert(static_cast<Uint>(CompileOptionBit::ZeroBaseVertex) < (1u << 16),
|
||||
"CompileOptionBit values must stay below bit 16: GetOrCreateProgram packs the "
|
||||
"default-framebuffer height into the high half of the same memo key");
|
||||
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
||||
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
||||
: flags.GetRaw();
|
||||
@@ -3027,6 +3050,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero
|
||||
// there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw
|
||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||
// and for the baseVertex word of an indexed indirect command.
|
||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||
Vector<Uint> zeroedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||
zeroedSpirv)) {
|
||||
moduleSpirvs[i] = std::move(zeroedSpirv);
|
||||
} else {
|
||||
// Failing open keeps the native builtin, which is the pre-fix behavior:
|
||||
// gl_BaseVertex reads firstVertex on a DrawArrays instead of zero.
|
||||
MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed "
|
||||
"draws will read the draw's first vertex from it instead of zero",
|
||||
program.GetExternalIndex());
|
||||
}
|
||||
}
|
||||
|
||||
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||
|
||||
@@ -60,6 +60,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PositionYFlip (the two are the same fact about the same draws) except under a
|
||||
// quarter turn, which this renderer does not convert rectangles for either.
|
||||
FragCoordYFlip = 1 << 7,
|
||||
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
|
||||
// as zero for every drawing command that has no baseVertex parameter - all the
|
||||
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
|
||||
// for a non-indexed draw whose program actually reads the builtin, so nothing else
|
||||
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
@@ -129,6 +135,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
|
||||
// program variant for non-indexed draws, and is deliberately a property of the
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -179,6 +190,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -192,6 +204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -231,6 +244,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -244,6 +258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -341,6 +356,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
|
||||
// draw with this program has to take the ZeroBaseVertex variant.
|
||||
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
|
||||
@@ -5821,7 +5821,40 @@ void main() {
|
||||
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
||||
}
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
// GL's gl_BaseVertex is zero for every command without a baseVertex parameter, while
|
||||
// Vulkan's builtin reports the draw's firstVertex; a non-indexed draw therefore takes
|
||||
// the zeroed program variant. The question is about the program's SPIR-V, not about
|
||||
// this draw, so it is memoized on (program lifetime, backend-state version): only the
|
||||
// very first draw of a program pays the extra lookup, and a program used exclusively
|
||||
// with non-indexed draws never resolves - never compiles, never re-stamps - the
|
||||
// variant no draw of it would use.
|
||||
const Bool nonIndexedDraw = !(aspects & DrawSetupAspect::IndexBuffer);
|
||||
const Uint64 baseVertexProgramLifetimeId = program.GetLifetimeId();
|
||||
const Uint32 baseVertexProgramVersion = program.GetBackendStateVersion();
|
||||
const Bool baseVertexQueryKnown = m_lastBaseVertexQueryValid &&
|
||||
m_lastBaseVertexProgramLifetimeId == baseVertexProgramLifetimeId &&
|
||||
m_lastBaseVertexProgramVersion == baseVertexProgramVersion;
|
||||
if (baseVertexQueryKnown && nonIndexedDraw && m_lastBaseVertexReads) {
|
||||
transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
}
|
||||
const ProgramFactory::VkProgramObject* resolvedProgramObj =
|
||||
&m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
if (!baseVertexQueryKnown) {
|
||||
// Read the answer out of the entry BEFORE any second lookup: that lookup may
|
||||
// insert and move every entry of the open-addressing cache, dangling the
|
||||
// reference. The zeroing pass leaves the variable declared, so the variant just
|
||||
// resolved answers the same as the base one either way.
|
||||
const Bool readsBaseVertex = resolvedProgramObj->readsBaseVertexBuiltin;
|
||||
m_lastBaseVertexQueryValid = true;
|
||||
m_lastBaseVertexProgramLifetimeId = baseVertexProgramLifetimeId;
|
||||
m_lastBaseVertexProgramVersion = baseVertexProgramVersion;
|
||||
m_lastBaseVertexReads = readsBaseVertex;
|
||||
if (nonIndexedDraw && readsBaseVertex) {
|
||||
transformFlags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
resolvedProgramObj = &m_programFactory->GetOrCreateProgram(program, transformFlags);
|
||||
}
|
||||
}
|
||||
const auto& programObj = *resolvedProgramObj;
|
||||
// For the snapshot's memoised entry pointer: if anything below inserts into the
|
||||
// program cache (blit/aux program compiles), the epoch moves and the snapshot
|
||||
// stores no pointer for this draw - the fast path then re-looks-up once.
|
||||
|
||||
@@ -773,6 +773,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||
|
||||
// Does the current program's vertex stage declare the BaseVertex builtin? A property
|
||||
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
|
||||
//
|
||||
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
|
||||
// variant, and a program that only ever draws non-indexed would then compile a variant
|
||||
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
|
||||
// it. With the memo the answer is known before the first lookup and only the variant
|
||||
// the draw actually needs is resolved.
|
||||
Bool m_lastBaseVertexQueryValid = false;
|
||||
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
|
||||
Uint32 m_lastBaseVertexProgramVersion = 0;
|
||||
Bool m_lastBaseVertexReads = false;
|
||||
|
||||
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||
// render-pass caches are open-addressing maps whose entries move on
|
||||
// insert, so no pointers into them are cached; the pipeline handle is
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
@@ -758,6 +759,15 @@ namespace MobileGL {
|
||||
outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(ZeroBaseVertexPass::CreateZeroBaseVertexPass());
|
||||
|
||||
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -65,6 +65,12 @@ namespace MobileGL {
|
||||
static bool LowerRectImages(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
|
||||
// read becomes zero, which is what GL defines for a command carrying no
|
||||
// baseVertex parameter while Vulkan's builtin would report firstVertex.
|
||||
// See ZeroBaseVertexPass.
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.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 "ZeroBaseVertexPass.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_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Returns the Input OpVariable decorated with |builtin|, or nullptr if none.
|
||||
Instruction* FindBuiltinInputVariable(IRContext* context, spv::BuiltIn builtin) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
for (auto& annotation : context->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status ZeroBaseVertexPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
Instruction* baseVertexVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::BaseVertex);
|
||||
if (baseVertexVar == nullptr) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t baseVertexVarId = baseVertexVar->result_id();
|
||||
|
||||
// Collect every load before mutating: rewriting invalidates the use list.
|
||||
//
|
||||
// Every OTHER kind of user is enumerated and refused rather than ignored. A read
|
||||
// that reaches the variable through a copied pointer or a pointer function
|
||||
// parameter would keep Vulkan's firstVertex while the pass still reported
|
||||
// success, i.e. a partial rewrite indistinguishable from a complete one. glslang
|
||||
// emits neither shape from GLSL today, so this fails closed on something that
|
||||
// cannot happen yet rather than silently half-doing it when it can.
|
||||
std::vector<Instruction*> baseVertexLoads;
|
||||
Bool sawUnexpectedUser = false;
|
||||
defUseMgr->ForEachUser(baseVertexVar, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpLoad:
|
||||
if (user->GetSingleWordInOperand(0) == baseVertexVarId) {
|
||||
baseVertexLoads.push_back(user);
|
||||
} else {
|
||||
sawUnexpectedUser = true;
|
||||
}
|
||||
return;
|
||||
// Declarations of the variable, not reads of it.
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpDecorateId:
|
||||
case spv::Op::OpDecorateString:
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return;
|
||||
default:
|
||||
sawUnexpectedUser = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (sawUnexpectedUser) {
|
||||
return Status::Failure;
|
||||
}
|
||||
if (baseVertexLoads.empty()) {
|
||||
// Declared but never read - the variant is already the shader itself.
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Materialize every zero constant BEFORE touching a single instruction, so the
|
||||
// constant/type managers are never consulted against a module this pass has
|
||||
// already half-rewritten - and so the rewrite loop below cannot fail partway
|
||||
// and leave one behind.
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
std::vector<uint32_t> zeroIds(baseVertexLoads.size(), 0);
|
||||
for (size_t i = 0; i < baseVertexLoads.size(); ++i) {
|
||||
// The zero is built from the LOAD's own type, because a shader may declare
|
||||
// the builtin as either int or uint.
|
||||
const uint32_t typeId = baseVertexLoads[i]->type_id();
|
||||
const spvtools::opt::analysis::Type* type = typeMgr->GetType(typeId);
|
||||
if (type == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const spvtools::opt::analysis::Constant* zero = constantMgr->GetConstant(type, {0u});
|
||||
if (zero == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
const Instruction* zeroInst = constantMgr->GetDefiningInstruction(zero, typeId);
|
||||
if (zeroInst == nullptr) {
|
||||
return Status::Failure;
|
||||
}
|
||||
zeroIds[i] = zeroInst->result_id();
|
||||
}
|
||||
|
||||
// `OpLoad %ty %res %baseVertex` becomes `OpCopyObject %ty %res %zero`. Keeping
|
||||
// %res makes every downstream use pick the zero up with no further rewriting.
|
||||
for (size_t i = 0; i < baseVertexLoads.size(); ++i) {
|
||||
baseVertexLoads[i]->SetOpcode(spv::Op::OpCopyObject);
|
||||
baseVertexLoads[i]->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {zeroIds[i]}}});
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken ZeroBaseVertexPass::CreateZeroBaseVertexPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<ZeroBaseVertexPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,40 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.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 "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL and Vulkan disagree about gl_BaseVertex on NON-INDEXED draws: GL defines it as
|
||||
// "the value passed to the baseVertex parameter, or zero for a command that has
|
||||
// none", so every DrawArrays form reads zero, while Vulkan's BaseVertex builtin
|
||||
// carries the draw's firstVertex there. (For indexed draws both mean the same thing,
|
||||
// GL's basevertex / Vulkan's vertexOffset, so those must keep the native builtin.)
|
||||
//
|
||||
// This pass produces the non-indexed variant of a vertex shader by replacing every
|
||||
// read of the BaseVertex builtin with a constant zero. The variable itself is left
|
||||
// declared - removing it would also have to reason about the DrawParameters
|
||||
// capability that a BaseInstance read in the same module still needs.
|
||||
//
|
||||
// Vulkan backend only, and only for the ZeroBaseVertex program variant: the
|
||||
// DirectGLES path has no BaseVertex builtin at all (see LowerDrawParametersPass).
|
||||
class ZeroBaseVertexPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "zero-base-vertex"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateZeroBaseVertexPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user