mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user