[Feat] (DirectVulkan, MG_Util): honour a glVertexAttribDivisor other than 1

Vulkan's VK_VERTEX_INPUT_RATE_INSTANCE advances an attribute once per instance and has
no way to say anything else, so every non-zero divisor collapsed to 1: an attribute the
application asked to change every three instances changed every one, and
KHR-GL40.draw_indirect.basic-drawArrays-instancing and its elements sibling drew the
wrong colours from instance one onward.

VK_EXT_vertex_attribute_divisor is exactly this state, so it is enabled when the device
has it and the per-binding divisors ride into the pipeline through
VkPipelineVertexInputDivisorStateCreateInfoEXT. Only divisors other than 1 are listed -
1 is what the plain input rate already means - and they join the layout hash, so two
layouts that differ only in a divisor no longer share a pipeline.

POST reports the feature either way, because without it the failure is silent and looks
like a shader bug: the attribute is fetched, just from the wrong instance. The GLES side
gains the two checks this session's other work made load-bearing for the same reason -
glPatchParameteri (without it GL_PATCH_VERTICES stays at the driver's 3 and a patch draw
of any other size renders nothing) and the transform feedback object entry points
(without them a second object cannot open a capture while the first is paused).

KHR-GL40.draw_indirect on Magma: 70/70 but for the arbitrary primitive-restart index,
which Vulkan cannot express at all.
This commit is contained in:
BZLZHH
2026-08-04 19:25:32 -04:00
parent a50b2c422b
commit cff959b2e8
5 changed files with 99 additions and 0 deletions
@@ -87,6 +87,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -180,6 +181,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0);
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
// else needs the extension to say it.
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
}
}
const auto& state = builder.Build();
@@ -191,6 +197,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
@@ -207,6 +214,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
for (const auto& divisor : entry.bindingDivisors) {
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
@@ -224,6 +235,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
if (!entry.bindingDivisors.empty()) {
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
entry.state.pNext = &entry.divisorState;
} else {
entry.state.pNext = nullptr;
}
return entry;
}