[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.)
This commit is contained in:
2026-07-11 01:11:23 -04:00
parent e18d369adf
commit e9fa99e16b
6 changed files with 147 additions and 1 deletions
@@ -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]);
@@ -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;
@@ -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;
@@ -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<void*>(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<Uint32>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
@@ -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;
+25
View File
@@ -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;