[Fix] (MG_State, MG_Impl, MG_Backend): conformant current generic vertex attribute values

GL 3.3 Core: a shader input whose generic attribute array is disabled reads that
attribute's current value (per-context state, default (0,0,0,1)). Four defects made
that path non-conformant, three of them silently.

* Out-of-bounds current-value reads. m_currentVertexAttributes held 16 entries while
  the DirectVulkan draw path walked shader input locations 0..31 and GL_MAX_VERTEX_ATTRIBS
  was advertised straight from the device (commonly 32). The only guard was MOBILEGL_ASSERT,
  which expands to nothing outside debug builds. Grow the storage capacity to 32, advertise
  min(device limit, capacity), validate against that dynamic limit, and give the accessors
  real runtime bounds checks. Replace the literal 32 loops with the constant, and pin
  MAX_VERTEX_ATTRIBS to the Uint32 mask width and to vertexInputTypes' bound with
  static_asserts so the two can no longer drift apart -- that drift was the bug.

* DirectGLES never fed current values to the driver. Values were stored in MG_State only,
  so a disabled attribute always rendered as the ES driver's own untouched (0,0,0,1) while
  DirectVulkan rendered it correctly: identical GL code, different pixels per backend.
  Add SyncCurrentVertexAttributeValues() to the draw prologue, and hoist the
  glType -> (base type, component count) dispatch into MG_State::GLState so both backends
  resolve the semantics from one place instead of it living inside VulkanRenderer.

* Enabled arrays the backend could not map were silently demoted to the current value.
  ToVkVertexFormat had no DataType::Float16 case, so a GL_HALF_FLOAT array fell to
  VK_FORMAT_UNDEFINED, dropped out of the vertex input state, and became indistinguishable
  from a disabled array: the geometry rendered a constant colour with GL_NO_ERROR. Add the
  Float16 mapping, track an unsupportedAttribMask, and hard-fail the draw before pipeline
  creation so no synthetic attribute is baked into a cached VkPipeline.

* glGetVertexAttrib{fv,iv,Iiv,Iuiv}(GL_CURRENT_VERTEX_ATTRIB) returned before any index
  validation, reading past the array instead of raising GL_INVALID_VALUE.

Also resolve ProgramObject::DoReflection's "TODO: get from backend" 16-location clamp,
which capped the new DirectGLES sync at locations 0..15; report GL_MAX_VERTEX_ATTRIBS
through the same helper the validators use, so the clamp cannot be bypassed; and bound
vertex binding indices by the same dynamic limit, since the default attribute -> binding
mapping is the identity.

Add a "Vertex attributes" driver POST row to both backends: FAIL below the GL 3.3 Core
minimum of 16, WARN above MobileGL's storage capacity (clamped, extra attributes unusable),
PASS in between -- making the driver/host mismatch that caused the out-of-bounds read
visible instead of silently swallowed.

Covered by 7 new regression tests (each verified to fail against the previous behaviour).
This commit is contained in:
2026-07-10 11:23:16 -04:00
parent eb090c6170
commit d40f753983
18 changed files with 539 additions and 30 deletions
+52
View File
@@ -12,6 +12,9 @@
#include <MGGitHash.h>
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
// Only for the compile-time MAX_VERTEX_ATTRIBS constant asserted below. The POST still executes no
// MG_State code: it runs standalone, before MG_State::Init().
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
#include <chrono>
#include <thread>
@@ -167,6 +170,49 @@ namespace MobileGL::MG_Util::SelfTest {
: "";
}
// ---- Vertex attribute limit --------------------------------------------
// GL 3.3 Core mandates GL_MAX_VERTEX_ATTRIBS >= 16 (spec table 6.32); a driver below
// that cannot back a conformant core context at all.
constexpr Int kGL33MinVertexAttribs = 16;
// The capacity of the per-context current-vertex-attribute array, which is also the width of
// the Uint32 attribute masks the backends pass around. Pinned to the state layer's constant so
// the two can never drift: a mismatch between them is precisely the defect this row guards.
constexpr Int kMobileGLMaxVertexAttribs = MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS;
static_assert(kMobileGLMaxVertexAttribs <= 32, "Vertex attribute masks are Uint32");
static_assert(kMobileGLMaxVertexAttribs >= kGL33MinVertexAttribs,
"MobileGL cannot advertise a conformant GL 3.3 Core GL_MAX_VERTEX_ATTRIBS");
// Both backends index a fixed-size, per-context array of current generic vertex attribute
// values by shader input location, and both clamp the GL_MAX_VERTEX_ATTRIBS they advertise
// to that array's capacity. A driver reporting more attributes than the array can hold used
// to make the DirectVulkan draw path walk locations past the end of it -- an out-of-bounds
// read in release builds, and a MOBILEGL_ASSERT abort in debug builds -- as soon as a shader
// declared a vertex input at a high location whose array was disabled. The clamp closes that
// hole, so this row exists to make the underlying driver/host mismatch visible rather than
// silently swallowed.
void EvaluateVertexAttribLimit(ReportBuilder& builder, Int deviceLimit, const char* rowName,
const char* driverLimitName) {
if (deviceLimit < kGL33MinVertexAttribs) {
builder.Fail(rowName,
format("{} = {} (< {}); OpenGL 3.3 Core requires at least {} generic vertex "
"attributes, so this driver cannot back a conformant core context",
driverLimitName, deviceLimit, kGL33MinVertexAttribs, kGL33MinVertexAttribs));
return;
}
if (deviceLimit > kMobileGLMaxVertexAttribs) {
builder.Warn(rowName,
format("{} = {} (> {}); MobileGL clamps GL_MAX_VERTEX_ATTRIBS to {} because its "
"current-vertex-attribute storage and its Uint32 attribute masks hold {} "
"locations, so the driver's extra attributes stay unusable",
driverLimitName, deviceLimit, kMobileGLMaxVertexAttribs,
kMobileGLMaxVertexAttribs, kMobileGLMaxVertexAttribs));
return;
}
builder.Pass(rowName, format("{} = {}; MobileGL advertises GL_MAX_VERTEX_ATTRIBS = {}",
driverLimitName, deviceLimit, deviceLimit));
}
void EvaluateGlesChecklist(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& glesFuncs) {
const Int major = caps.GLESVersion.Major;
@@ -185,6 +231,9 @@ namespace MobileGL::MG_Util::SelfTest {
versionLabel + " (< 3.1: no compute shaders or native indirect draws)");
}
EvaluateVertexAttribLimit(builder, caps.MaxVertexAttribs, "Vertex attributes",
"GL_MAX_VERTEX_ATTRIBS");
if (es31) {
GLint maxVertexSsboBlocks = 0;
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
@@ -1006,6 +1055,9 @@ namespace MobileGL::MG_Util::SelfTest {
VkApiVersionToString(properties.apiVersion)));
}
EvaluateVertexAttribLimit(builder, static_cast<Int>(properties.limits.maxVertexInputAttributes),
"Vertex attributes", "maxVertexInputAttributes");
Vector<VkExtensionProperties> deviceExtensions;
Uint32 deviceExtensionCount = 0;
if (vkEnumerateDeviceExtensionPropertiesFn(physicalDevice, nullptr, &deviceExtensionCount, nullptr) ==