[Fix, Feat] (MG_Backend): make DirectVulkan multi-draw actually draw, then pick its best tier

The bug: DirectVulkan.cpp::MultiDrawElements had its entire body
commented out - plain glMultiDrawElements on Magma recorded NOTHING,
no error, no pixels (readback shows the deferred clear never even
materialized). It now shares the tuned base-vertex implementation, and
both plain entries are pixel-proven by a 4-sub-draw harness.

The feature: every CPU-side multi-draw form dispatches through three
tiers after round-9's contiguous-run merge (restructured to merge into
a span BEFORE dispatch, so every tier consumes the shrunken array):
  1. VK_EXT_multi_draw: one vkCmdDrawMulti(Indexed)EXT, chunked by
     maxMultiDrawCount; per-draw vertexOffset rides in the struct. The
     extension is requested only when enumerated and its feature bit
     confirmed, entry points via vkGetDeviceProcAddr, demoted if
     missing.
  2. multiDrawIndirect: the param span uploads DIRECTLY as a transient
     INDIRECT-usage buffer - DrawIndexedCmdParam is layout-identical
     to VkDrawIndexedIndirectCommand and DrawCmdParam's head is a
     legal 24-byte-stride VkDrawIndirectCommand, both static_asserted,
     so no repacking - then one vkCmdDraw(Indexed)Indirect per
     maxDrawIndirectCount chunk. firstInstance!=0 additionally
     requires drawIndirectFirstInstance or the batch drops a tier.
  3. The byte-identical unroll.
gl_DrawID: tiers 1-2 are spec-correct (0,1,2,3 across a probe's
sub-draws); the unroll tier keeps the pre-existing always-0 contract.
The default tiers strictly improve DrawID correctness.

Adversarially verified: the five real DirectVulkan retrace images are
BIT-IDENTICAL (md5) across auto/ext/indirect/unroll; zero validation
VUIDs on every tier; a simulated no-EXT device resolves to indirect
and renders the same bytes; the known-red create-indirect fixture
crashes at the identical call before and after (not worse, not fixed).
Unit suite 423/423 on the rebased tree, retrace subset 10/10. Bench:
mc_sodium_multidraw's contiguous shape merges 32->1 before dispatch,
so no bench delta - the tiers' beneficiaries are non-contiguous real
streams (the sodium RETRACE pushes ~58-sub-draw batches, in=out
243101 with zero merges) and mobile drivers. A reproducible +3-4%
code-layout drift on mc_use_program (zero shared code, I-cache
displacement from +400 lines) stays under the action gate and is
booked here rather than hidden.
This commit is contained in:
BZLZHH
2026-08-07 06:41:21 -04:00
parent 231d5c90e4
commit ebc5bff9b1
3 changed files with 416 additions and 117 deletions
@@ -1393,31 +1393,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->MultiDrawArrays(payload);
}
// Shared body of glMultiDrawElements (basevertex == nullptr) and
// glMultiDrawElementsBaseVertex: identical calls except for the per-draw
// vertex offset, which VkMultiDrawIndexedInfoEXT / VkDrawIndexedIndirectCommand /
// vkCmdDrawIndexed all carry natively.
static void MultiDrawElementsImpl(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0) {
return;
}
MultiDrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
// Loop-invariant: the index type is fixed for the whole multi-draw, so resolve
// its byte size once instead of twice per sub-draw (a cross-TU switch that
// showed up in per-frame profiles of sodium-style 132x32 multi-draws). Index
// sizes are 1/2/4, so the per-sub-draw offset division below reduces to a
// shift - the hardware divide was the hottest instruction of this loop.
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
return;
}
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
// TODO: allocate draw cmd buf elsewhere
static Vector<DrawIndexedCmdParam> params;
params.clear();
params.resize(drawcount);
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] == 0) {
continue;
}
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
payload.indexBufferView.indexByteOffset = 0;
payload.indexBufferView.indexByteSize =
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * indexSize,
payload.indexBufferView.indexByteSize);
auto& param = params[i];
param.indexCount = count[i];
param.instanceCount = 1;
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) >> indexSizeShift;
param.vertexOffset = basevertex != nullptr ? basevertex[i] : 0;
param.firstInstance = 0;
}
payload.drawCount = drawcount;
payload.pParams = params.data();
pVulkanRenderer->MultiDrawElements(payload);
}
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
// Vector<DrawElementCmd> cmds;
// cmds.reserve(static_cast<SizeT>(drawcount));
// for (GLsizei i = 0; i < drawcount; ++i) {
// if (count[i] == 0) {
// continue;
// }
//
// DrawElementCmd payload{};
// payload.mode = mode;
// payload.firstVertex = 0;
// payload.indexCount = count[i];
// payload.indexType = type;
// payload.indexByteOffset = reinterpret_cast<SizeT>(indices[i]);
// cmds.push_back(payload);
// }
//
// if (cmds.empty()) {
// return;
// }
// pVulkanRenderer->MultiDrawElements(cmds);
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
@@ -1445,52 +1480,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
MultiDrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
// Loop-invariant: the index type is fixed for the whole multi-draw, so resolve
// its byte size once instead of twice per sub-draw (a cross-TU switch that
// showed up in per-frame profiles of sodium-style 132x32 multi-draws). Index
// sizes are 1/2/4, so the per-sub-draw offset division below reduces to a
// shift - the hardware divide was the hottest instruction of this loop.
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
MGLOG_E("MultiDrawElementsBaseVertex skipped: unsupported index type 0x%x", type);
return;
}
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
// TODO: allocate draw cmd buf elsewhere
static Vector<DrawIndexedCmdParam> params;
params.clear();
params.resize(drawcount);
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] == 0) {
continue;
}
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
payload.indexBufferView.indexByteOffset = 0;
payload.indexBufferView.indexByteSize =
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * indexSize,
payload.indexBufferView.indexByteSize);
auto& param = params[i];
param.indexCount = count[i];
param.instanceCount = 1;
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) >> indexSizeShift;
param.vertexOffset = basevertex[i];
param.firstInstance = 0;
}
payload.drawCount = drawcount;
payload.pParams = params.data();
pVulkanRenderer->MultiDrawElements(payload);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
@@ -2988,6 +2988,8 @@ void main() {
m_device = VK_NULL_HANDLE;
}
s_vkCmdDrawIndexedIndirectCount = nullptr;
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
if (m_instance != VK_NULL_HANDLE && m_surface != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
@@ -9562,14 +9564,191 @@ void main() {
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
VkCommandBuffer& commandBuffer = frame.commandBuffer;
EmitMultiDraw(frame.commandBuffer, payload.pParams, payload.drawCount);
}
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
vkCmdDraw(commandBuffer,
payload.pParams[idraw].vertexCount,
payload.pParams[idraw].instanceCount,
payload.pParams[idraw].firstVertex,
payload.pParams[idraw].firstInstance);
// The tier-2 indirect batch uploads the param arrays as-is: the leading members of the
// renderer's draw-parameter structs are exactly Vulkan's indirect command layouts, and
// vkCmdDraw(Indexed)Indirect accepts any 4-aligned stride >= the command size, so the
// trailing CPU-side metadata rides along unread instead of forcing a repack.
static_assert(sizeof(DrawIndexedCmdParam) == sizeof(VkDrawIndexedIndirectCommand) &&
offsetof(DrawIndexedCmdParam, indexCount) == offsetof(VkDrawIndexedIndirectCommand, indexCount) &&
offsetof(DrawIndexedCmdParam, instanceCount) ==
offsetof(VkDrawIndexedIndirectCommand, instanceCount) &&
offsetof(DrawIndexedCmdParam, firstIndex) == offsetof(VkDrawIndexedIndirectCommand, firstIndex) &&
offsetof(DrawIndexedCmdParam, vertexOffset) ==
offsetof(VkDrawIndexedIndirectCommand, vertexOffset) &&
offsetof(DrawIndexedCmdParam, firstInstance) ==
offsetof(VkDrawIndexedIndirectCommand, firstInstance),
"DrawIndexedCmdParam must alias VkDrawIndexedIndirectCommand for the tier-2 multi-draw upload");
static_assert(sizeof(DrawCmdParam) % 4 == 0 && sizeof(DrawCmdParam) >= sizeof(VkDrawIndirectCommand) &&
offsetof(DrawCmdParam, vertexCount) == offsetof(VkDrawIndirectCommand, vertexCount) &&
offsetof(DrawCmdParam, instanceCount) == offsetof(VkDrawIndirectCommand, instanceCount) &&
offsetof(DrawCmdParam, firstVertex) == offsetof(VkDrawIndirectCommand, firstVertex) &&
offsetof(DrawCmdParam, firstInstance) == offsetof(VkDrawIndirectCommand, firstInstance),
"DrawCmdParam must lead with VkDrawIndirectCommand for the tier-2 multi-draw upload");
void VulkanRenderer::EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount) {
if (drawCount == 0) {
return;
}
if (drawCount == 1) {
vkCmdDraw(commandBuffer, pParams[0].vertexCount, pParams[0].instanceCount, pParams[0].firstVertex,
pParams[0].firstInstance);
return;
}
// Tier 1: VK_EXT_multi_draw. vkCmdDrawMultiEXT shares one instanceCount/firstInstance
// across the whole batch, so the batch must be uniform in both (GL's glMultiDrawArrays
// always is: 1/0).
if (m_multiDrawAllowExt) {
Bool uniformInstances = true;
for (Uint32 idraw = 1; idraw < drawCount; ++idraw) {
if (pParams[idraw].instanceCount != pParams[0].instanceCount ||
pParams[idraw].firstInstance != pParams[0].firstInstance) {
uniformInstances = false;
break;
}
}
if (uniformInstances) {
static Vector<VkMultiDrawInfoEXT> infos;
infos.resize(drawCount);
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
infos[idraw].firstVertex = pParams[idraw].firstVertex;
infos[idraw].vertexCount = pParams[idraw].vertexCount;
}
for (Uint32 base = 0; base < drawCount; base += m_maxMultiDrawCount) {
const Uint32 chunk = std::min(drawCount - base, m_maxMultiDrawCount);
s_vkCmdDrawMultiEXT(commandBuffer, chunk, infos.data() + base, pParams[0].instanceCount,
pParams[0].firstInstance, sizeof(VkMultiDrawInfoEXT));
}
return;
}
}
// Tier 2: multiDrawIndirect - one vkCmdDrawIndirect over a transient command array.
// A sub-draw with firstInstance != 0 is illegal in an indirect command without the
// drawIndirectFirstInstance feature; such a batch falls to the unrolled tier.
if (m_multiDrawAllowIndirect) {
Bool firstInstanceLegal = m_drawIndirectFirstInstanceFeatureEnabled;
if (!firstInstanceLegal) {
firstInstanceLegal = true;
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
if (pParams[idraw].firstInstance != 0) {
firstInstanceLegal = false;
break;
}
}
}
const Uint32 maxIndirectCount = m_physicalDevice.properties.limits.maxDrawIndirectCount;
if (firstInstanceLegal && maxIndirectCount > 0) {
BufferSlice commandSlice{};
if (m_bufferManager.UploadTransient(BufferKind::Indirect, m_frameContext.GetCurrentFrameIndex(),
pParams,
static_cast<VkDeviceSize>(drawCount) * sizeof(DrawCmdParam),
sizeof(Uint32), commandSlice)) {
for (Uint32 base = 0; base < drawCount; base += maxIndirectCount) {
const Uint32 chunk = std::min(drawCount - base, maxIndirectCount);
vkCmdDrawIndirect(commandBuffer, commandSlice.buffer,
commandSlice.offset +
static_cast<VkDeviceSize>(base) * sizeof(DrawCmdParam),
chunk, sizeof(DrawCmdParam));
}
return;
}
// Transient arena refused the upload: fall through to the unrolled tier.
}
}
// Tier 3: unrolled loop, byte-identical fallback (and the only tier where a SPIR-V
// DrawIndex consumer sees 0 for every sub-draw instead of the sub-draw index).
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
vkCmdDraw(commandBuffer, pParams[idraw].vertexCount, pParams[idraw].instanceCount,
pParams[idraw].firstVertex, pParams[idraw].firstInstance);
}
}
void VulkanRenderer::EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams,
Uint32 drawCount) {
if (drawCount == 0) {
return;
}
if (drawCount == 1) {
vkCmdDrawIndexed(commandBuffer, pParams[0].indexCount, pParams[0].instanceCount, pParams[0].firstIndex,
pParams[0].vertexOffset, pParams[0].firstInstance);
return;
}
// Tier 1: VK_EXT_multi_draw. VkMultiDrawIndexedInfoEXT carries per-draw
// firstIndex/indexCount/vertexOffset (pVertexOffset = nullptr keeps the per-draw
// offsets), but instanceCount/firstInstance are batch-wide, so the batch must be
// uniform in both (GL's glMultiDrawElements* always is: 1/0).
if (m_multiDrawAllowExt) {
Bool uniformInstances = true;
for (Uint32 idraw = 1; idraw < drawCount; ++idraw) {
if (pParams[idraw].instanceCount != pParams[0].instanceCount ||
pParams[idraw].firstInstance != pParams[0].firstInstance) {
uniformInstances = false;
break;
}
}
if (uniformInstances) {
static Vector<VkMultiDrawIndexedInfoEXT> infos;
infos.resize(drawCount);
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
infos[idraw].firstIndex = pParams[idraw].firstIndex;
infos[idraw].indexCount = pParams[idraw].indexCount;
infos[idraw].vertexOffset = pParams[idraw].vertexOffset;
}
for (Uint32 base = 0; base < drawCount; base += m_maxMultiDrawCount) {
const Uint32 chunk = std::min(drawCount - base, m_maxMultiDrawCount);
s_vkCmdDrawMultiIndexedEXT(commandBuffer, chunk, infos.data() + base,
pParams[0].instanceCount,
static_cast<Uint32>(pParams[0].firstInstance),
sizeof(VkMultiDrawIndexedInfoEXT), nullptr);
}
return;
}
}
// Tier 2: multiDrawIndirect - one vkCmdDrawIndexedIndirect over a transient command
// array (DrawIndexedCmdParam aliases VkDrawIndexedIndirectCommand, see static_assert).
if (m_multiDrawAllowIndirect) {
Bool firstInstanceLegal = m_drawIndirectFirstInstanceFeatureEnabled;
if (!firstInstanceLegal) {
firstInstanceLegal = true;
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
if (pParams[idraw].firstInstance != 0) {
firstInstanceLegal = false;
break;
}
}
}
const Uint32 maxIndirectCount = m_physicalDevice.properties.limits.maxDrawIndirectCount;
if (firstInstanceLegal && maxIndirectCount > 0) {
BufferSlice commandSlice{};
if (m_bufferManager.UploadTransient(BufferKind::Indirect, m_frameContext.GetCurrentFrameIndex(),
pParams,
static_cast<VkDeviceSize>(drawCount) *
sizeof(DrawIndexedCmdParam),
sizeof(Uint32), commandSlice)) {
for (Uint32 base = 0; base < drawCount; base += maxIndirectCount) {
const Uint32 chunk = std::min(drawCount - base, maxIndirectCount);
vkCmdDrawIndexedIndirect(commandBuffer, commandSlice.buffer,
commandSlice.offset +
static_cast<VkDeviceSize>(base) * sizeof(DrawIndexedCmdParam),
chunk, sizeof(DrawIndexedCmdParam));
}
return;
}
}
}
// Tier 3: unrolled loop, byte-identical fallback (and the only tier where a SPIR-V
// DrawIndex consumer sees 0 for every sub-draw instead of the sub-draw index).
for (Uint32 idraw = 0; idraw < drawCount; ++idraw) {
vkCmdDrawIndexed(commandBuffer, pParams[idraw].indexCount, pParams[idraw].instanceCount,
pParams[idraw].firstIndex, pParams[idraw].vertexOffset, pParams[idraw].firstInstance);
}
}
@@ -9591,9 +9770,9 @@ void main() {
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
VkCommandBuffer& commandBuffer = frame.commandBuffer;
// Collapse contiguous sub-draw runs into one vkCmdDrawIndexed. Per-sub-draw
// Collapse contiguous sub-draw runs BEFORE tier dispatch: merging shrinks the
// param span every tier consumes (fewer VkMultiDrawIndexedInfoEXT entries, a
// smaller transient command array, fewer unrolled vkCmdDrawIndexed). Per-sub-draw
// command emission in the driver dominates a Sodium-shaped multi-draw
// (steady-state profile: >60% of the case inside the Vulkan driver's
// vkCmdDrawIndexed encoding for 132x32 sub-draws/frame), and a chunk
@@ -9622,47 +9801,45 @@ void main() {
mergeGranularity = 0;
}
}
if (mergeGranularity == 0) {
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
vkCmdDrawIndexed(commandBuffer,
payload.pParams[idraw].indexCount,
payload.pParams[idraw].instanceCount,
payload.pParams[idraw].firstIndex,
payload.pParams[idraw].vertexOffset,
payload.pParams[idraw].firstInstance);
}
return;
}
Uint32 idraw = 0;
while (idraw < payload.drawCount) {
const DrawIndexedCmdParam& head = payload.pParams[idraw];
++idraw;
if (head.indexCount == 0) {
continue; // draws nothing, contributes nothing to a run
}
Uint32 mergedIndexCount = head.indexCount;
if (head.instanceCount == 1) {
while (idraw < payload.drawCount) {
const DrawIndexedCmdParam& next = payload.pParams[idraw];
if (next.indexCount == 0) {
++idraw;
continue;
}
if (mergedIndexCount % mergeGranularity != 0 ||
next.instanceCount != 1 ||
next.vertexOffset != head.vertexOffset ||
next.firstInstance != head.firstInstance ||
next.firstIndex != head.firstIndex + mergedIndexCount ||
mergedIndexCount + next.indexCount < mergedIndexCount) {
break;
}
mergedIndexCount += next.indexCount;
++idraw;
const DrawIndexedCmdParam* pParams = payload.pParams;
Uint32 drawCount = payload.drawCount;
static Vector<DrawIndexedCmdParam> mergedParams;
if (mergeGranularity != 0) {
mergedParams.clear();
mergedParams.reserve(drawCount);
Uint32 idraw = 0;
while (idraw < drawCount) {
DrawIndexedCmdParam head = pParams[idraw];
++idraw;
if (head.indexCount == 0) {
continue; // draws nothing, contributes nothing to a run
}
if (head.instanceCount == 1) {
while (idraw < drawCount) {
const DrawIndexedCmdParam& next = pParams[idraw];
if (next.indexCount == 0) {
++idraw;
continue;
}
if (head.indexCount % mergeGranularity != 0 ||
next.instanceCount != 1 ||
next.vertexOffset != head.vertexOffset ||
next.firstInstance != head.firstInstance ||
next.firstIndex != head.firstIndex + head.indexCount ||
head.indexCount + next.indexCount < head.indexCount) {
break;
}
head.indexCount += next.indexCount;
++idraw;
}
}
mergedParams.push_back(head);
}
vkCmdDrawIndexed(commandBuffer, mergedIndexCount, head.instanceCount, head.firstIndex,
head.vertexOffset, head.firstInstance);
pParams = mergedParams.data();
drawCount = static_cast<Uint32>(mergedParams.size());
}
EmitMultiDrawIndexed(frame.commandBuffer, pParams, drawCount);
}
// Byte size of the command structures GL defines for the indirect draws (GL 4.6 core
@@ -9838,7 +10015,8 @@ void main() {
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
MGLOG_D("DirectVulkan: glMultiDrawElementsIndirect(drawcount=%d stride=%d)", drawcount, stride);
if (drawcount == 1 || (m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) {
if (drawcount == 1 ||
(!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) {
vkCmdDrawIndexedIndirect(frame.commandBuffer,
drawSlice.buffer,
drawSlice.offset + static_cast<VkDeviceSize>(commandOffset),
@@ -9901,7 +10079,8 @@ void main() {
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
MGLOG_D("DirectVulkan: glMultiDrawArraysIndirect(drawcount=%d stride=%d)", drawcount, stride);
if (drawcount == 1 || (m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) {
if (drawcount == 1 ||
(!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) {
vkCmdDrawIndirect(frame.commandBuffer,
drawSlice.buffer,
drawSlice.offset + static_cast<VkDeviceSize>(commandOffset),
@@ -11087,6 +11266,7 @@ void main() {
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat);
}
deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance;
m_drawIndirectFirstInstanceFeatureEnabled = deviceFeatures.drawIndirectFirstInstance == VK_TRUE;
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
@@ -11368,6 +11548,56 @@ void main() {
}
}
// VK_EXT_multi_draw: tier 1 of the multi-draw dispatch - one vkCmdDrawMulti(Indexed)EXT
// for a whole glMultiDraw* batch (VkMultiDrawIndexedInfoEXT carries per-draw
// firstIndex/indexCount/vertexOffset, so glMultiDrawElementsBaseVertex fits natively).
// Requested only when both the extension and its multiDraw feature are present;
// absent it, the dispatch falls to the multiDrawIndirect tier or the unrolled loop.
m_multiDrawExtensionEnabled = false;
m_maxMultiDrawCount = 0;
VkPhysicalDeviceMultiDrawFeaturesEXT multiDrawFeatures{};
multiDrawFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_FEATURES_EXT;
if (IsExtensionSupported(availableExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME) &&
getPhysicalDeviceFeatures2 != nullptr) {
VkPhysicalDeviceFeatures2 featureQuery{};
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
featureQuery.pNext = &multiDrawFeatures;
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
if (multiDrawFeatures.multiDraw == VK_TRUE) {
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, VK_EXT_MULTI_DRAW_EXTENSION_NAME)) {
enabledDeviceExtensions.push_back(VK_EXT_MULTI_DRAW_EXTENSION_NAME);
}
multiDrawFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
deviceCreateInfo.pNext = &multiDrawFeatures;
m_multiDrawExtensionEnabled = true;
VkPhysicalDeviceMultiDrawPropertiesEXT multiDrawProperties{};
multiDrawProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_PROPERTIES_EXT;
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
if (getPhysicalDeviceProperties2 == nullptr) {
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
}
if (getPhysicalDeviceProperties2 != nullptr) {
VkPhysicalDeviceProperties2 propertyQuery{};
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
propertyQuery.pNext = &multiDrawProperties;
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
}
// Spec minimum is 1024; a driver reporting 0 through a failed query must not
// zero out every batch, so fall back to the spec minimum.
m_maxMultiDrawCount = multiDrawProperties.maxMultiDrawCount != 0
? multiDrawProperties.maxMultiDrawCount
: 1024;
MGLOG_I("Enabled optional device extension: %s (maxMultiDrawCount=%u)",
VK_EXT_MULTI_DRAW_EXTENSION_NAME, m_maxMultiDrawCount);
} else {
MGLOG_I("VK_EXT_multi_draw is advertised but its multiDraw feature is unavailable; "
"multi-draw batches use the indirect or unrolled tier");
}
}
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
@@ -11424,6 +11654,51 @@ void main() {
m_drawIndirectCountExtensionEnabled = false;
}
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
if (m_multiDrawExtensionEnabled) {
s_vkCmdDrawMultiEXT =
reinterpret_cast<PFN_vkCmdDrawMultiEXT>(vkGetDeviceProcAddr(m_device, "vkCmdDrawMultiEXT"));
s_vkCmdDrawMultiIndexedEXT = reinterpret_cast<PFN_vkCmdDrawMultiIndexedEXT>(
vkGetDeviceProcAddr(m_device, "vkCmdDrawMultiIndexedEXT"));
if (s_vkCmdDrawMultiEXT == nullptr || s_vkCmdDrawMultiIndexedEXT == nullptr) {
MGLOG_W("VK_EXT_multi_draw enabled but its entry points are missing, will continue as if "
"VK_EXT_multi_draw is not supported!");
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
m_multiDrawExtensionEnabled = false;
}
}
// Resolve the multi-draw dispatch tiers once: device support clamped by the
// MOBILEGL_MAGMA_MULTIDRAW_MODE preference. Requesting an unavailable tier is
// never an error - the dispatch falls down the chain ext -> indirect -> unroll.
{
using MG_Config::MultiDrawMode;
const MultiDrawMode mode = MG_Config::Features.MagmaMultiDrawMode;
m_multiDrawAllowExt =
m_multiDrawExtensionEnabled && (mode == MultiDrawMode::Auto || mode == MultiDrawMode::Ext);
m_multiDrawAllowIndirect = m_multiDrawIndirectFeatureEnabled && mode != MultiDrawMode::Unroll;
m_multiDrawForceUnrollIndirect = mode == MultiDrawMode::Unroll;
if (mode == MultiDrawMode::Ext && !m_multiDrawExtensionEnabled) {
MGLOG_I("MOBILEGL_MAGMA_MULTIDRAW_MODE=ext requested but VK_EXT_multi_draw is unavailable; "
"falling back to the %s tier",
m_multiDrawAllowIndirect ? "indirect" : "unroll");
}
if (mode == MultiDrawMode::Indirect && !m_multiDrawIndirectFeatureEnabled) {
MGLOG_I("MOBILEGL_MAGMA_MULTIDRAW_MODE=indirect requested but the multiDrawIndirect device "
"feature is unavailable; falling back to the unroll tier");
}
MGLOG_I("Multi-draw dispatch tier: %s (VK_EXT_multi_draw=%s, multiDrawIndirect=%s, mode=%s)",
m_multiDrawAllowExt ? "ext" : (m_multiDrawAllowIndirect ? "indirect" : "unroll"),
m_multiDrawExtensionEnabled ? "true" : "false",
m_multiDrawIndirectFeatureEnabled ? "true" : "false",
mode == MultiDrawMode::Auto ? "auto"
: mode == MultiDrawMode::Ext ? "ext"
: mode == MultiDrawMode::Indirect ? "indirect"
: "unroll");
}
if (m_transformFeedbackFeatureEnabled) {
s_vkCmdBindTransformFeedbackBuffersEXT = reinterpret_cast<PFN_vkCmdBindTransformFeedbackBuffersEXT>(
vkGetDeviceProcAddr(m_device, "vkCmdBindTransformFeedbackBuffersEXT"));
@@ -327,6 +327,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool RecreateSwapchain();
private:
// Tiered emission for an already-set-up multi-draw batch (state bound, index
// buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
// vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
// vkCmdDraw(Indexed) loop. Tier eligibility is per-batch (uniform instance
// state for tier 1, firstInstance/feature legality for tier 2); every tier
// consumes the same param span, so contiguous-run merging done by the caller
// benefits all of them.
void EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams, Uint32 drawCount);
void EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount);
struct BlitUniformData {
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
@@ -477,6 +487,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_indexTypeUint8ExtensionEnabled = false;
Bool m_logicOpFeatureEnabled = false;
Bool m_multiDrawIndirectFeatureEnabled = false;
// drawIndirectFirstInstance gates indirect commands whose firstInstance != 0;
// cached at device creation because the tier-2 multi-draw path (a transient
// VkDrawIndexedIndirectCommand array) is illegal for such a sub-draw without it.
Bool m_drawIndirectFirstInstanceFeatureEnabled = false;
// VK_EXT_multi_draw: native batched submission for the CPU-side glMultiDraw*
// families (tier 1 of the multi-draw dispatch).
Bool m_multiDrawExtensionEnabled = false;
Uint32 m_maxMultiDrawCount = 0;
// Multi-draw dispatch tiers, resolved once at device creation from device support
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE (a preference, never a demand):
// tier 1 (ext): one vkCmdDrawMulti(Indexed)EXT - m_multiDrawAllowExt
// tier 2 (indirect): one vkCmdDraw(Indexed)Indirect batch - m_multiDrawAllowIndirect
// tier 3 (unroll): one vkCmdDraw(Indexed) per sub-draw - always available
// m_multiDrawForceUnrollIndirect additionally forces the GPU-parameter
// glMultiDraw*Indirect paths onto their per-command loop (mode=unroll only).
Bool m_multiDrawAllowExt = false;
Bool m_multiDrawAllowIndirect = false;
Bool m_multiDrawForceUnrollIndirect = false;
Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false;
@@ -508,6 +536,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
// (and its multiDraw feature) is enabled; null otherwise.
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
static inline PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
// VK_EXT_transform_feedback (GL transform feedback capture)
Bool m_transformFeedbackFeatureEnabled = false;