diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 54ec77fb..91f549cd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -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(std::countr_zero(indexSize)); + + // TODO: allocate draw cmd buf elsewhere + static Vector 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(indices[i]) + count[i] * indexSize, + payload.indexBufferView.indexByteSize); + + auto& param = params[i]; + + param.indexCount = count[i]; + param.instanceCount = 1; + param.firstIndex = reinterpret_cast(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 cmds; - // cmds.reserve(static_cast(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(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(std::countr_zero(indexSize)); - - // TODO: allocate draw cmd buf elsewhere - static Vector 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(indices[i]) + count[i] * indexSize, - payload.indexBufferView.indexByteSize); - - auto& param = params[i]; - - param.indexCount = count[i]; - param.instanceCount = 1; - param.firstIndex = reinterpret_cast(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, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 23ae45ae..23be2196 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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 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(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(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 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(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(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(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 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(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(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(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(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( + vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2")); + if (getPhysicalDeviceProperties2 == nullptr) { + getPhysicalDeviceProperties2 = reinterpret_cast( + 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(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(vkGetDeviceProcAddr(m_device, "vkCmdDrawMultiEXT")); + s_vkCmdDrawMultiIndexedEXT = reinterpret_cast( + 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( vkGetDeviceProcAddr(m_device, "vkCmdBindTransformFeedbackBuffersEXT")); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 82cf7807..0fec1674 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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;