[Fix, Test] (BackendLoader, DirectGLES, DirectVulkan, GLImpl): answer the layer and viewport-index provoking-vertex conventions from the backend

This commit is contained in:
2026-08-20 15:40:25 -04:00
parent 51883cf1a3
commit d24d5b5ccd
8 changed files with 205 additions and 6 deletions
+14
View File
@@ -384,6 +384,20 @@ namespace MobileGL {
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8;
Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
@@ -1336,6 +1336,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
// describe behaviour this backend does not implement.
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -912,6 +912,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxClipDistances =
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
// Assigned explicitly rather than left to the struct's defaults, like every other
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
+14 -6
View File
@@ -1572,9 +1572,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_LINE_WIDTH:
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
return;
case GL_LAYER_PROVOKING_VERTEX:
*params = GL_LAST_VERTEX_CONVENTION;
return;
case GL_LOGIC_OP_MODE:
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
return;
@@ -2117,9 +2114,6 @@ namespace MobileGL::MG_Impl::GLImpl {
params[3] = vp.w();
return;
}
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
*params = GL_LAST_VERTEX_CONVENTION;
return;
case GL_MAX_ELEMENT_INDEX:
*params = 1024 * 1024; // TODO
return;
@@ -2207,6 +2201,20 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_CLIP_DISTANCES:
*params = dynamicParameters.MaxClipDistances;
break;
// Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
// 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
// wherever they do not actually pin a convention - claiming one is a statement about
// which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
// rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
// DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
// accepts all four values, and .provoking_vertex - which failed on both devices, in
// OPPOSITE directions - stops verifying as soon as either answer is undefined.
case GL_LAYER_PROVOKING_VERTEX:
*params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
break;
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
break;
@@ -57,6 +57,12 @@ namespace {
GLint maxViewports = 32;
GLint viewportSubpixelBits = 8;
bool viewportArrayLimitsQueried = false;
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core; GL_VIEWPORT_INDEX_PROVOKING_VERTEX comes with
// GL_OES_viewport_array. Both must go unasked where they do not exist, and a driver answer
// outside the four legal conventions must not be forwarded as one.
GLint layerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
GLint viewportIndexProvokingVertex = GL_LAST_VERTEX_CONVENTION;
bool layerProvokingVertexQueried = false;
// A driver rejecting one of the UNCONDITIONAL probes. GL_SMOOTH_LINE_WIDTH_RANGE is the
// realistic one - it is desktop-only state that every GLES driver refuses - and it stands
// in for the whole run: whatever it leaves behind must not reach the application.
@@ -195,6 +201,14 @@ namespace {
g_fake.viewportArrayLimitsQueried = true;
*data = g_fake.viewportSubpixelBits;
break;
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
g_fake.viewportArrayLimitsQueried = true;
*data = g_fake.viewportIndexProvokingVertex;
break;
case GL_LAYER_PROVOKING_VERTEX:
g_fake.layerProvokingVertexQueried = true;
*data = g_fake.layerProvokingVertex;
break;
case GL_MAX_COLOR_TEXTURE_SAMPLES:
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
case GL_MAX_FRAMEBUFFER_SAMPLES:
@@ -782,6 +796,62 @@ TEST(ViewportArrayCapabilities, TheLimitsAreOnlyAskedForWhenTheExtensionIsPresen
EXPECT_EQ(withCaps.ViewportSubpixelBits, g_fake.viewportSubpixelBits);
}
// GL_LAYER_PROVOKING_VERTEX and GL_VIEWPORT_INDEX_PROVOKING_VERTEX name which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. MobileGL used to answer a hard-coded
// GL_LAST_VERTEX_CONVENTION for both, derived from nothing, and got it wrong on both test devices
// in OPPOSITE directions. GL_UNDEFINED_VERTEX is a legal answer (GL 4.6 table 23.65) and it is
// the honest one wherever the capability that would give the convention meaning is absent.
TEST(ProvokingVertexConventions, AreTakenFromTheDriverOnlyWhereThePnameExists) {
const auto funcs = MakeFakeGLESFunctions();
// ES 3.1, no viewport array: neither pname exists, so neither is asked for.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_FALSE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(es31Caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
EXPECT_EQ(es31Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
// ES 3.2 with the viewport array: both exist and both driver answers come through verbatim.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
g_fake.extensions.emplace_back("GL_OES_viewport_array");
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(es32Caps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(es32Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_LAST_VERTEX_CONVENTION));
// ES 3.2 WITHOUT the viewport array - the shape of both test devices. The layer convention is
// real and comes from the driver; the viewport-index one describes a selection that never
// happens, because only viewport 0 is ever rasterized, and stays undefined.
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities deviceLikeCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(deviceLikeCaps, funcs));
EXPECT_EQ(deviceLikeCaps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(deviceLikeCaps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
// A driver answering something that is not one of the four legal conventions must not have it
// forwarded as one: GL_UNDEFINED_VERTEX describes "MobileGL cannot tell you" exactly.
TEST(ProvokingVertexConventions, AnIllegalDriverAnswerBecomesUndefined) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
g_fake.layerProvokingVertex = 0x1234;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
EXPECT_EQ(caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
// The multisample ceilings are ES 3.1 state; a driver that answers zero - or an older context
// that answers nothing - must not have that reach GL_Getter, which would then reject the sample
// count it just advertised.
+27
View File
@@ -665,6 +665,33 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
}
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
// reports GL_UNDEFINED_VERTEX, which GL 4.6 table 23.65 permits and which is what the backend
// honestly implements - the provoking mode is chosen per pipeline out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology.
TEST(ProvokingVertexConventions, EachBackendReportsWhatItActuallyPins) {
using namespace MobileGL;
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
MG_External::GLESCapabilities glesCaps;
glesCaps.LayerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
glesCaps.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
EXPECT_EQ(glesBackend.GetDynamicParameters().LayerProvokingVertex,
static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
EXPECT_EQ(glesBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
MG_External::VulkanCapabilities vkCaps;
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
EXPECT_EQ(vkBackend.GetDynamicParameters().LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
EXPECT_EQ(vkBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
}
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
using namespace MobileGL;
@@ -831,6 +831,35 @@ namespace MobileGL::MG_Util::BackendLoader {
return includesBase;
}
// GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and
// GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL
// cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder
// - is the accurate thing to say about it.
static GLenum NormalizeProvokingVertexConvention(GLint driverValue) {
switch (static_cast<GLenum>(driverValue)) {
case GL_FIRST_VERTEX_CONVENTION:
case GL_LAST_VERTEX_CONVENTION:
case GL_PROVOKING_VERTEX:
case GL_UNDEFINED_VERTEX:
return static_cast<GLenum>(driverValue);
default:
return GL_UNDEFINED_VERTEX;
}
}
static const char* ProvokingVertexConventionName(GLenum convention) {
switch (convention) {
case GL_FIRST_VERTEX_CONVENTION:
return "GL_FIRST_VERTEX_CONVENTION";
case GL_LAST_VERTEX_CONVENTION:
return "GL_LAST_VERTEX_CONVENTION";
case GL_PROVOKING_VERTEX:
return "GL_PROVOKING_VERTEX";
default:
return "GL_UNDEFINED_VERTEX";
}
}
Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) {
if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) {
MGLOG_E("Required GLES functions are not loaded, cannot query capabilities");
@@ -1073,6 +1102,11 @@ namespace MobileGL::MG_Util::BackendLoader {
// clipping program silently rendered nothing. The guarded probe below only ever widens it.
GLint maxClipDistances = 0;
GLint maxViewports = 16;
// GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal
// answer rather than a placeholder: with neither geometry shaders nor a viewport array
// there is no layered or multi-viewport draw for a convention to describe.
GLenum layerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum viewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
GLfloat minFragmentInterpolationOffset = -0.5f;
GLfloat maxFragmentInterpolationOffset = 0.4375f;
GLint fragmentInterpolationOffsetBits = 4;
@@ -1263,6 +1297,21 @@ namespace MobileGL::MG_Util::BackendLoader {
}
}
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
// what gl_Layer needs). Ask the driver where the pname exists rather than asserting a
// convention: it is a statement about which vertex of a primitive supplies gl_Layer, and
// MobileGL forwards the geometry stage to the driver rather than implementing the
// selection itself, so the driver's answer IS MobileGL's answer. Below ES 3.2 there are
// no layered draws to have a convention for and GL_UNDEFINED_VERTEX stands, which GL 4.6
// table 23.65 explicitly permits.
if (esAtLeast32) {
GLint driverLayerConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
drainErrors();
glesFuncs.glGetIntegerv(GL_LAYER_PROVOKING_VERTEX, &driverLayerConvention);
if (!drainErrors()) {
layerProvokingVertex = NormalizeProvokingVertexConvention(driverLayerConvention);
}
}
// GL_MAX_VIEWPORTS (0x825B), GL_VIEWPORT_SUBPIXEL_BITS (0x825C) and GL_VIEWPORT_BOUNDS_RANGE
// (0x825D) all arrive with GL_OES_viewport_array and exist nowhere in ES core, so on the
// drivers DirectGLES actually runs on all three raise GL_INVALID_ENUM. The values MobileGL
@@ -1272,9 +1321,11 @@ namespace MobileGL::MG_Util::BackendLoader {
// hold), floors GL_SUBPIXEL_BITS at its own 4, and the bounds range is clamped to the core
// minimum below. What changes is that the errors stop being manufactured.
if (caps.SupportsViewportArray) {
GLint driverViewportIndexConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
drainErrors();
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
glesFuncs.glGetIntegerv(GL_VIEWPORT_INDEX_PROVOKING_VERTEX, &driverViewportIndexConvention);
if (glesFuncs.glGetFloatv) {
glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
}
@@ -1285,6 +1336,9 @@ namespace MobileGL::MG_Util::BackendLoader {
viewportSubpixelBits = 0;
viewportBoundsRange[0] = -32768.0f;
viewportBoundsRange[1] = 32767.0f;
} else {
viewportIndexProvokingVertex =
NormalizeProvokingVertexConvention(driverViewportIndexConvention);
}
}
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
@@ -1452,6 +1506,8 @@ namespace MobileGL::MG_Util::BackendLoader {
// extension the probe above never ran at all - so the flag, not the local, decides.
caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0;
caps.MaxViewports = maxViewports;
caps.LayerProvokingVertex = layerProvokingVertex;
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
caps.MaxViewportWidth = maxViewportDims[0];
caps.MaxViewportHeight = maxViewportDims[1];
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
@@ -1543,6 +1599,9 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
caps.ViewportBoundsRangeMax);
MGLOG_I(" GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
MGLOG_I(" GL_LAYER_PROVOKING_VERTEX: %s", ProvokingVertexConventionName(caps.LayerProvokingVertex));
MGLOG_I(" GL_VIEWPORT_INDEX_PROVOKING_VERTEX: %s",
ProvokingVertexConventionName(caps.ViewportIndexProvokingVertex));
caps.IndirectDrawInstanceIdIncludesBaseInstance =
ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
@@ -1280,6 +1280,11 @@ namespace MobileGL {
// in FillInGLESCapabilities.
Int MaxClipDistances = 0;
Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX
// (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what
// a driver that has neither is honestly saying.
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;