mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +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/LowerDrawParametersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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/NormalizeRectCoordinatesPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.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
|
// 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.
|
// detects the case so the user gets one warning instead of silent corruption.
|
||||||
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
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) {
|
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
|
||||||
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
|
||||||
if (variable != nullptr &&
|
if (variable != nullptr &&
|
||||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||||
variable->built_in == SpvBuiltInInstanceIndex) {
|
variable->built_in == builtin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2244,6 +2258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeVertexInputLocationMask = 0;
|
entry.activeVertexInputLocationMask = 0;
|
||||||
entry.vertexInputTypes.fill(0);
|
entry.vertexInputTypes.fill(0);
|
||||||
|
entry.readsBaseVertexBuiltin = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
||||||
@@ -2265,6 +2280,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.readsBaseVertexBuiltin = ReflectedReadsBaseVertexBuiltin(reflectModule);
|
||||||
|
|
||||||
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
|
||||||
static Bool s_warnedInstanceIndexUnsupported = false;
|
static Bool s_warnedInstanceIndexUnsupported = false;
|
||||||
if (!s_warnedInstanceIndexUnsupported) {
|
if (!s_warnedInstanceIndexUnsupported) {
|
||||||
@@ -2909,6 +2926,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// a FragCoordYFlip variant also depends on the baked default-framebuffer height, so
|
// 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
|
// 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.
|
// 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)
|
const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip)
|
||||||
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
? (flags.GetRaw() | (m_defaultFramebufferHeight << 16))
|
||||||
: flags.GetRaw();
|
: 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
|
// 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
|
// 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
|
// 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
|
// 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.
|
// quarter turn, which this renderer does not convert rectangles for either.
|
||||||
FragCoordYFlip = 1 << 7,
|
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 CompileOptionFlags = Flags<CompileOptionBit>;
|
||||||
using HashType = Uint64;
|
using HashType = Uint64;
|
||||||
@@ -129,6 +135,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
Bool fragmentReplacesDepth = false;
|
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
|
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||||
@@ -179,6 +190,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
@@ -192,6 +204,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.readsBaseVertexBuiltin = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
@@ -231,6 +244,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
|
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||||
lastUsedFrame = other.lastUsedFrame;
|
lastUsedFrame = other.lastUsedFrame;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
@@ -244,6 +258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
other.fragmentReplacesDepth = false;
|
other.fragmentReplacesDepth = false;
|
||||||
|
other.readsBaseVertexBuiltin = false;
|
||||||
other.lastUsedFrame = 0;
|
other.lastUsedFrame = 0;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -341,6 +356,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
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:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
|
|||||||
@@ -5821,7 +5821,40 @@ void main() {
|
|||||||
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
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
|
// 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
|
// 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.
|
// 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_lastLodBaseFlags = {};
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
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
|
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
|
||||||
// render-pass caches are open-addressing maps whose entries move on
|
// render-pass caches are open-addressing maps whose entries move on
|
||||||
// insert, so no pointers into them are cached; the pipeline handle is
|
// insert, so no pointers into them are cached; the pipeline handle is
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||||
|
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||||
@@ -758,6 +759,15 @@ namespace MobileGL {
|
|||||||
outputBinary);
|
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,
|
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ namespace MobileGL {
|
|||||||
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
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
|
// 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
|
// (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
|
// 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