From 6ea7ccdf64ba78a71df4abede7dbe58b705a19be Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Tue, 4 Aug 2026 20:00:17 -0400 Subject: [PATCH] [Feat] (DirectVulkan): support an arbitrary primitive restart index Vulkan restarts only on the fixed all-ones value of the index type, so GL_PRIMITIVE_RESTART with a glPrimitiveRestartIndex of anything else used to hard-fail the draw. GL_PRIMITIVE_RESTART_FIXED_INDEX already matches Vulkan and is untouched. Rewrite the indices into a transient copy instead, substituting the fixed value for the application's. An index that already equals the fixed value would then be indistinguishable from a restart, so it is nudged down by one: it can only be a real index, since the application's restart index is a different number, and the vertex it names is outside any well-defined draw -- whereas leaving it alone would tear the primitive in two. The element array buffer is rewritten whole rather than only the drawn range, because an indirect draw's firstIndex lives in GPU memory and cannot be adjusted from here; every element therefore keeps its position. --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 82 ++++++++++++++++--- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 731ef167..98a8036e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3292,6 +3292,47 @@ void main() { return true; } + namespace { + // Copies index data, replacing every occurrence of the application's arbitrary restart + // index with the fixed all-ones value of the index type - the only one Vulkan restarts + // on. An index that already equals the fixed value would then be indistinguishable from + // a restart, so it is nudged to the next-lowest value: it can only be a real index (the + // application's restart index is a different number), and the vertex it selects is + // outside any well-defined draw anyway, whereas leaving it alone would tear the + // primitive in two. + void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType, + Uint32 applicationRestartIndex, Vector& output) { + output.resize(sizeBytes); + if (sizeBytes == 0 || source == nullptr) { + return; + } + Memcpy(output.data(), source, sizeBytes); + const auto rewrite = [&](auto* indices, auto fixedMax) { + const SizeT count = sizeBytes / sizeof(*indices); + for (SizeT i = 0; i < count; ++i) { + if (indices[i] == static_cast(applicationRestartIndex)) { + indices[i] = fixedMax; + } else if (indices[i] == fixedMax) { + indices[i] = fixedMax - 1; + } + } + }; + switch (indexType) { + case VK_INDEX_TYPE_UINT8: + rewrite(reinterpret_cast(output.data()), static_cast(0xFFu)); + break; + case VK_INDEX_TYPE_UINT16: + rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFu)); + break; + case VK_INDEX_TYPE_UINT32: + rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFFFFFu)); + break; + default: + break; + } + } + } // namespace + Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame, const MG_State::GLState::VertexArrayObject& vao, const IndexBufferView* pIndexBufferView) { @@ -3315,8 +3356,10 @@ void main() { // GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan // only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX - // already matches that, so only the arbitrary form needs checking; hard-fail at this draw with - // the reason if the index is not the fixed value (a fallback would silently drop restarts). + // already matches that, so only the arbitrary form needs handling: rewrite the indices into a + // transient copy where the application's restart index becomes the fixed one. + Uint32 substituteRestartIndex = 0; + Bool substituteRestart = false; if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) && !MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex(); @@ -3327,15 +3370,8 @@ void main() { case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break; default: break; } - if (restartIndex != fixedMax) { - THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + - std::to_string(restartIndex) + - ") is not supported by the Vulkan backend, which only restarts on the fixed index " - "value (" + - std::to_string(fixedMax) + - ") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX, or set " - "glPrimitiveRestartIndex to that value."); - } + substituteRestart = restartIndex != fixedMax; + substituteRestartIndex = restartIndex; } const auto* indexBuffer = @@ -3349,9 +3385,16 @@ void main() { MGLOG_E("DrawElements skipped: no element array buffer bound and no client index data"); return false; } + Vector rewrittenIndices; + const void* uploadSource = clientIndices; + if (substituteRestart) { + RewriteRestartIndices(clientIndices, pIndexBufferView->indexByteSize, vkIndexType, + substituteRestartIndex, rewrittenIndices); + uploadSource = rewrittenIndices.data(); + } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - clientIndices, pIndexBufferView->indexByteSize, 4, slice)) { + uploadSource, pIndexBufferView->indexByteSize, 4, slice)) { MGLOG_E("DrawElements skipped: failed to upload client index data"); return false; } @@ -3373,7 +3416,20 @@ void main() { BufferSlice slice{}; auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); - if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { + if (substituteRestart) { + // The whole buffer is rewritten, not just this draw's range, so that every element + // index keeps its position: an indirect draw's firstIndex lives in GPU memory and + // cannot be adjusted from here. + indexBufferShared->SyncGpuWrites(); + Vector rewrittenIndices; + RewriteRestartIndices(indexBufferShared->MappedData(), indexBufferShared->GetSize(), vkIndexType, + substituteRestartIndex, rewrittenIndices); + if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), + rewrittenIndices.data(), rewrittenIndices.size(), 4, slice)) { + MGLOG_E("DrawElements skipped: failed to upload restart-substituted index data"); + return false; + } + } else if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data"); if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer");