[Feat] (DirectVulkan): emulate GL_LINE_LOOP with closed indexed line strips

Vulkan has no LINE_LOOP topology and the frontend used to reject the mode
with GL_INVALID_OPERATION, which is itself non-conformant (several KHR-GL33
transform_feedback tests draw line loops and expect no error). DrawArrays,
DrawElements and DrawElementsBaseVertex now rewrite the draw into an
indexed GL_LINE_STRIP whose synthesized uint32 index list revisits the
first vertex, delivered through the client-memory index path (a new
forceClientMemory flag keeps a bound element-array buffer from hijacking
the synthesized pointer). Entry points without the rewrite degrade to an
open line strip instead of a triangle list.
This commit is contained in:
BZLZHH
2026-07-31 16:25:46 -04:00
parent b9ecfef0b6
commit 92140405c1
5 changed files with 95 additions and 11 deletions
@@ -1250,10 +1250,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->Clear(mask);
}
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
// whose synthesized index list revisits the first vertex at the end.
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
DrawIndexedCmd payload{};
payload.mode = GL_LINE_STRIP;
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
payload.indexBufferView.forceClientMemory = true;
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
payload.params.instanceCount = 1;
payload.params.vertexOffset = basevertex;
pVulkanRenderer->DrawElements(payload);
}
// Resolve a DrawElements index list (bound element-array buffer or client
// memory) into uint32 values with the loop-closing first index appended.
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
Vector<Uint32>& outIndices) {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0 || count < 2) {
return false;
}
const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices);
const SizeT bufferSize = indexBufferShared->GetSize();
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
return false;
}
indexBufferShared->SyncPersistentMappedRange();
indexBytes = indexBufferShared->MappedData() + offset;
} else {
indexBytes = static_cast<const Uint8*>(indices);
if (indexBytes == nullptr) {
return false;
}
}
outIndices.resize(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
switch (indexSize) {
case 1: outIndices[i] = indexBytes[i]; break;
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
}
}
outIndices[count] = outIndices[0];
return true;
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) {
if (count < 2) {
return;
}
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
closedIndices[i] = static_cast<Uint32>(first + i);
}
closedIndices[count] = static_cast<Uint32>(first);
DrawLineLoopAsIndexedStrip(closedIndices, 0);
return;
}
DrawCmd payload{};
payload.mode = mode;
payload.params.firstVertex = first;
@@ -1266,6 +1332,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, 0);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -1334,6 +1408,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -2848,7 +2848,9 @@ void main() {
}
const Uint8* indexBytes = nullptr;
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
const auto& indexBufferShared = indexView.forceClientMemory
? SharedPtr<MG_State::GLState::BufferObject>{}
: vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT bufferSize = indexBufferShared->GetSize();
if (indexBufferShared->MappedData() == nullptr || indexView.indexByteOffset > bufferSize ||
@@ -3262,7 +3264,8 @@ void main() {
}
}
const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get();
const auto* indexBuffer =
pIndexBufferView->forceClientMemory ? nullptr : vao.GetIndexBufferBindingSlot().GetBoundObject().get();
if (indexBuffer == nullptr) {
// No element-array buffer: the view's byte offset is a raw client pointer
// (desktop drivers accept client-memory indices and the GL CTS relies on
@@ -76,6 +76,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum indexType = GL_UNSIGNED_SHORT;
SizeT indexByteOffset = 0;
SizeT indexByteSize = 0;
// Interpret indexByteOffset as a raw client pointer even when an element
// array buffer is bound (backend-synthesized index lists, e.g. the
// GL_LINE_LOOP -> LINE_STRIP rewrite).
Bool forceClientMemory = false;
};
struct DrawIndexedCmd {
@@ -57,15 +57,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
return false;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
@@ -25,6 +25,11 @@ namespace MobileGL {
case GL_TRIANGLE_FAN:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
case GL_LINE_LOOP:
// DrawArrays/DrawElements rewrite line loops into closed indexed
// strips; entry points without that rewrite (instanced/indirect)
// degrade to an open strip, which only misses the closing segment.
MGLOG_W("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
default:
MGLOG_W("Unrecognized primitive topology");
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;