From e9fa99e16b5cf63eb737a2e2ed8641dd2d97a785 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 11 Jul 2026 01:11:23 -0400 Subject: [PATCH] [Feat] (MG_Backend): wire primitive restart into both backends; detect dualSrcBlend Make GL_PRIMITIVE_RESTART[_FIXED_INDEX] actually take effect at draw time, following the detect-at-init / POST / fallback-or-hard-fail discipline. DirectVulkan: - Thread primitiveRestartEnable through the pipeline (payload + hash + input-assembly), set from the GL_PRIMITIVE_RESTART / _FIXED_INDEX caps. - Detect and enable primitiveTopologyListRestart (VK_EXT_primitive_topology_list_restart) at device creation; cache it. Strip/fan restart needs no feature; a *list* topology with restart and no feature hard-fails at the draw with the reason. - Vulkan only restarts on the fixed all-ones index value, so an arbitrary GL_PRIMITIVE_RESTART index that is not that value hard-fails in UploadAndBindIndexBuffer (where the index type is known). - Also detect+enable and cache the dualSrcBlend base feature (groundwork for GL_SRC1_* dual-source blending). DirectGLES: - Sync GL_PRIMITIVE_RESTART_FIXED_INDEX from either restart cap (GLES core has only the fixed-index form); an arbitrary non-fixed index hard-fails in the indexed draw paths with the reason. POST: dualSrcBlend and primitiveTopologyListRestart capability rows (Pass when supported, Warn with the fallback/hard-fail consequence otherwise). Library builds clean; SanityTest 31/31. (The actual restart rendering and the hard-fail paths need a real GPU and are not runtime-testable here.) --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 41 +++++++++++ .../DirectVulkan/Renderer/PipelineFactory.cpp | 3 + .../DirectVulkan/Renderer/PipelineFactory.h | 1 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 73 ++++++++++++++++++- .../DirectVulkan/Renderer/VulkanRenderer.h | 5 ++ MobileGL/MG_Util/SelfTest/DriverPost.cpp | 25 +++++++ 6 files changed, 147 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 25ce3809..d914ecae 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -564,6 +564,18 @@ namespace MobileGL::MG_Backend::DirectGLES { #undef SYNC_CAPABILITY + { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed all-ones + // value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to + // it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements). + const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled; + const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled || + g_syncedRenderStateParameters.PrimitiveRestartEnabled; + if (restart != syncedRestart) { + restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX) + : g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX); + } + } + const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; }; { // Blend State @@ -1329,12 +1341,38 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glClear(mask); } + // GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed all-ones value). If the app + // enabled the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index, hard-fail at this draw with + // the reason (a fallback would silently drop restarts and corrupt geometry). + void CheckPrimitiveRestartSupported(GLenum indexType) { + if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { + return; + } + Uint32 fixedMax = 0; + switch (indexType) { + case GL_UNSIGNED_BYTE: fixedMax = 0xFFu; break; + case GL_UNSIGNED_SHORT: fixedMax = 0xFFFFu; break; + case GL_UNSIGNED_INT: fixedMax = 0xFFFFFFFFu; break; + default: return; + } + const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex(); + if (restartIndex != fixedMax) { + THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + std::to_string(restartIndex) + + ") is not supported by the GLES 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."); + } + } + void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); + CheckPrimitiveRestartSupported(type); g_GLESFuncs.glDrawElements(mode, count, type, indices); } @@ -1360,6 +1398,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); + CheckPrimitiveRestartSupported(type); g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex); } @@ -1390,6 +1429,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); + CheckPrimitiveRestartSupported(type); for (GLsizei i = 0; i < drawcount; ++i) { g_GLESFuncs.glDrawElements(mode, count[i], type, indices[i]); @@ -1403,6 +1443,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif DrawSyncBit syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); + CheckPrimitiveRestartSupported(type); for (GLsizei i = 0; i < drawcount; ++i) { g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], basevertex[i]); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index f1936577..75df6ee4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -126,6 +126,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); + XXHASH_VERIFY( + XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace))); @@ -212,6 +214,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; ia.topology = payload.topology; + ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE; VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; vpci.viewportCount = 1; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 867b9078..cec5ec07 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -29,6 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; Uint32 subpass = 0; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + Bool primitiveRestartEnable = false; VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT; VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index d4729e61..6201b64e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2266,6 +2266,31 @@ void main() { return false; } + // 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). + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) && + !MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) { + const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex(); + Uint32 fixedMax = 0; + switch (vkIndexType) { + case VK_INDEX_TYPE_UINT8: fixedMax = 0xFFu; break; + case VK_INDEX_TYPE_UINT16: fixedMax = 0xFFFFu; break; + 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."); + } + } + const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); MOBILEGL_ASSERT(indexBuffer != nullptr, "UploadAndBindIndexBuffer requires bound EBO"); const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; @@ -2973,6 +2998,25 @@ void main() { ? requestedPolygonMode : VK_POLYGON_MODE_FILL; + const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode); + const Bool primitiveRestartEnabled = + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + // Primitive restart on a *list* topology requires the primitiveTopologyListRestart feature; + // strip/fan restart works without it. Silently dropping restarts would corrupt geometry, so + // hard-fail here (at the draw) with the reason when the device lacks the feature. + const auto isListTopology = [](VkPrimitiveTopology t) { + return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || + t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || + t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY || + t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY || t == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + }; + if (primitiveRestartEnabled && !m_primitiveTopologyListRestartFeatureEnabled && isListTopology(vkTopology)) { + THROW_EXCEPTION("Primitive restart on a list topology requires the primitiveTopologyListRestart device " + "feature (VK_EXT_primitive_topology_list_restart), which this device does not support; use " + "a strip/fan topology or a device that supports it."); + } + PipelineFactory::PipelineCreatePayload payload { .programHash = programObj.hash, .vertexInputHash = vertexInputHash, @@ -2981,7 +3025,8 @@ void main() { .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .rasterizationSamples = renderPassEntry.sampleCount, .subpass = 0, - .topology = MG_Util::ConvertPrimitiveModeToVkEnum(mode), + .topology = vkTopology, + .primitiveRestartEnable = primitiveRestartEnabled, .polygonMode = effectivePolygonMode, .cullMode = cullFaceEnabled ? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise) @@ -6328,6 +6373,8 @@ void main() { m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid; m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE; + deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend; + m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE; deviceFeatures.logicOp = supportedDeviceFeatures.logicOp; deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance; deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance; @@ -6423,6 +6470,30 @@ void main() { MGLOG_W("shaderDrawParameters is unavailable; shaders using gl_DrawID/gl_BaseInstance will not work"); } + // primitiveTopologyListRestart lets primitive restart work on *list* topologies (strip/fan + // restart needs no feature). Optional; enabled via VK_EXT_primitive_topology_list_restart. + m_primitiveTopologyListRestartFeatureEnabled = false; + VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{}; + listRestartFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT; + if (IsExtensionSupported(availableExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME) && + getPhysicalDeviceFeatures2 != nullptr) { + VkPhysicalDeviceFeatures2 featureQuery{}; + featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + featureQuery.pNext = &listRestartFeatures; + getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); + if (listRestartFeatures.primitiveTopologyListRestart == VK_TRUE) { + if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, + VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) { + enabledDeviceExtensions.push_back(VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME); + } + listRestartFeatures.pNext = const_cast(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &listRestartFeatures; + m_primitiveTopologyListRestartFeatureEnabled = true; + MGLOG_I("Enabled optional device extension: %s", + VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME); + } + } + deviceCreateInfo.enabledExtensionCount = static_cast(enabledDeviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 002a7ac1..57d74b3e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -354,6 +354,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // drive a runtime fallback when the device lacks them. Bool m_fillModeNonSolidFeatureEnabled = false; Bool m_independentBlendFeatureEnabled = false; + // dualSrcBlend gates GL_SRC1_* blend factors (glBindFragDataLocationIndexed dual-source blend); + // primitiveTopologyListRestart gates primitive restart on *list* topologies (strip/fan restart + // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. + Bool m_dualSrcBlendFeatureEnabled = false; + Bool m_primitiveTopologyListRestartFeatureEnabled = false; // Cached at device creation from the graphics queue family properties // and device limits; drives timer-query support. Uint32 m_timestampValidBits = 0; diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index d46e284d..87fec742 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -1126,6 +1126,11 @@ namespace MobileGL::MG_Util::SelfTest { builder.Warn("independentBlend", "unsupported; per-draw-buffer glColorMaski falls back to draw buffer 0 for all attachments"); } + if (features.dualSrcBlend == VK_TRUE) { + builder.Pass("dualSrcBlend", "GL_SRC1_* dual-source blend factors supported"); + } else { + builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw"); + } Bool shaderDrawParameters = false; if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { @@ -1147,6 +1152,26 @@ namespace MobileGL::MG_Util::SelfTest { "unavailable; shaders using gl_DrawID/gl_BaseInstance will not work"); } + Bool primitiveTopologyListRestart = false; + if (vkGetPhysicalDeviceFeatures2Fn != nullptr && + HasVkExtension(deviceExtensions, VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME)) { + VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT listRestartFeatures{}; + listRestartFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT; + VkPhysicalDeviceFeatures2 features2{}; + features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features2.pNext = &listRestartFeatures; + vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2); + primitiveTopologyListRestart = listRestartFeatures.primitiveTopologyListRestart == VK_TRUE; + } + if (primitiveTopologyListRestart) { + builder.Pass("primitiveTopologyListRestart", + "primitive restart supported on list topologies (GL_PRIMITIVE_RESTART)"); + } else { + builder.Warn("primitiveTopologyListRestart", + "unsupported; primitive restart works on strip/fan topologies only, list-topology restart " + "hard-fails at draw"); + } + if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { VkPhysicalDeviceSubgroupProperties subgroupProperties{}; subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;