From 52c050131e0c3a95a5e87a1e4ebac9a72526514e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 25 Aug 2026 05:21:50 -0400 Subject: [PATCH] [Fix] (DirectGLES): advertise the vertex attributes whose layout(location) the driver's compiler will actually accept --- .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 18 +- MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp | 191 ++++++++++++++++++ MobileGL/MG_Util/SelfTest/DriverBugProbes.h | 47 +++++ 3 files changed, 255 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 5ac65a0f..a7d70502 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "Loader.h" +#include "MG_Util/SelfTest/DriverBugProbes.h" #include "MG_Util/Types.h" #include #include @@ -1505,7 +1506,22 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxVertexTextureImageUnits = maxVertexTextureImageUnits; caps.MaxComputeTextureImageUnits = maxComputeTextureImageUnits; caps.MaxCombinedTextureImageUnits = maxCombinedTextureImageUnits; - caps.MaxVertexAttribs = maxVertexAttribs; + // Not the driver's answer alone: MobileGL emits every vertex input as a + // layout(location = N) qualifier, so an attribute the driver counts but its ESSL + // compiler will not let anything DECLARE is not an attribute MobileGL can hand to an + // application. The probe measures where the qualifier actually stops (see + // SelfTest::ProbeExplicitVertexInputLocationCeiling - Adreno 830 advertises 32 and + // refuses the qualifier from 16 up) and answers with the advertised count on every + // driver that has no such gap and on any run that reaches no verdict, so this only ever + // lowers the number, and only on evidence. + const SelfTest::VertexInputLocationCeilingMeasurement& locationCeiling = + SelfTest::ExplicitVertexInputLocationCeiling(glesFuncs); + caps.MaxVertexAttribs = std::min(maxVertexAttribs, locationCeiling.usableLocations); + if (locationCeiling.detected) { + MGLOG_I(" GL_MAX_VERTEX_ATTRIBS reduced from the driver's %d to %d: " + "layout(location = N) on a vertex input is refused from N = %d upward", + maxVertexAttribs, caps.MaxVertexAttribs, locationCeiling.usableLocations); + } caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks; caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks; caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks; diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index c0b051f7..81075cb4 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -1348,6 +1348,196 @@ namespace MobileGL::MG_Util::SelfTest { } namespace { + // ===================== EXPLICIT VERTEX INPUT LOCATION CEILING ===================== + + constexpr const char* kAttributeLocationProbeName = "explicit vertex input location"; + + // COMPILES ONE VERTEX STAGE and reports nothing else. A link would drag in every other + // reason a program can be refused (varying budgets, the fragment stage, the linker's own + // location rules), and the defect this measures is in the driver's ESSL COMPILER: it + // rejects the declaration itself, before any of that can matter. + Bool ExplicitVertexInputLocationCompiles(const GLESFunctionsTable& gl, Int location, + String* firstRejectionMessage) { + const String source = format("#version 320 es\n" + "layout(location = {}) in vec4 a_probe;\n" + "void main() {{ gl_Position = a_probe; }}\n", + location); + Drain(gl); + const GLuint shader = gl.glCreateShader(GL_VERTEX_SHADER); + if (shader == 0) return false; + const char* text = source.c_str(); + gl.glShaderSource(shader, 1, &text, nullptr); + gl.glCompileShader(shader); + GLint compiled = GL_FALSE; + gl.glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled == GL_FALSE && firstRejectionMessage != nullptr && firstRejectionMessage->empty()) { + char log[256] = {0}; + gl.glGetShaderInfoLog(shader, static_cast(sizeof(log) - 1), nullptr, log); + // One line: the driver's own wording is the report's whole evidential value, and + // the rest of the log is the same sentence repeated per declaration. + String message = log; + if (const SizeT newline = message.find('\n'); newline != String::npos) { + message.resize(newline); + } + while (!message.empty() && (message.back() == ' ' || message.back() == '\r')) message.pop_back(); + *firstRejectionMessage = Move(message); + } + gl.glDeleteShader(shader); + Drain(gl); + return compiled != GL_FALSE; + } + + // THE SECOND CONTROL, and the one that decides whether the cap is about the LAYOUT + // QUALIFIER or about the attribute itself. The same input, declared with no qualifier at + // all and placed by glBindAttribLocation instead. If this links and glGetAttribLocation + // answers with the location asked for, the driver can address that attribute perfectly + // well and only the qualifier path is capped - which is what makes clamping the + // advertised count the right response rather than a shrug. If it fails too, the driver + // genuinely has fewer attributes than it advertises; the clamp is still correct, but the + // report must not claim the attribute is reachable another way. + Bool BindAttribLocationReaches(const GLESFunctionsTable& gl, Int location) { + if (!gl.glCreateProgram || !gl.glAttachShader || !gl.glBindAttribLocation || !gl.glLinkProgram || + !gl.glGetProgramiv || !gl.glGetAttribLocation || !gl.glDeleteProgram) { + return false; + } + constexpr const char* kVertexSource = "#version 320 es\n" + "in vec4 a_probe;\n" + "void main() { gl_Position = a_probe; }\n"; + constexpr const char* kFragmentSource = "#version 320 es\n" + "precision highp float;\n" + "out vec4 o_color;\n" + "void main() { o_color = vec4(1.0); }\n"; + Drain(gl); + const GLuint vertexShader = + CompileStage(gl, GL_VERTEX_SHADER, kVertexSource, "vertex", kAttributeLocationProbeName); + if (vertexShader == 0) return false; + const GLuint fragmentShader = + CompileStage(gl, GL_FRAGMENT_SHADER, kFragmentSource, "fragment", kAttributeLocationProbeName); + if (fragmentShader == 0) { + gl.glDeleteShader(vertexShader); + return false; + } + const GLuint program = gl.glCreateProgram(); + gl.glAttachShader(program, vertexShader); + gl.glAttachShader(program, fragmentShader); + gl.glBindAttribLocation(program, static_cast(location), "a_probe"); + gl.glLinkProgram(program); + GLint linked = GL_FALSE; + gl.glGetProgramiv(program, GL_LINK_STATUS, &linked); + const Bool reached = linked != GL_FALSE && gl.glGetAttribLocation(program, "a_probe") == location; + gl.glDeleteShader(vertexShader); + gl.glDeleteShader(fragmentShader); + gl.glDeleteProgram(program); + Drain(gl); + return reached; + } + } // namespace + + VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) { + VertexInputLocationCeilingMeasurement measurement; + // `usableLocations` is the number a caller clamps to, so it carries the driver's own + // answer from the first line onward and every early return below leaves it there. A + // probe that cannot run has to withdraw nothing at all, and a zero here would withdraw + // every attribute the device has. + if (gl.glGetIntegerv != nullptr) { + GLint advertisedEarly = 0; + gl.glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &advertisedEarly); + if (gl.glGetError != nullptr) Drain(gl); + measurement.advertisedMaxVertexAttribs = advertisedEarly; + measurement.usableLocations = advertisedEarly; + } + if (!gl.glCreateShader || !gl.glShaderSource || !gl.glCompileShader || !gl.glGetShaderiv || + !gl.glGetShaderInfoLog || !gl.glDeleteShader || !gl.glGetIntegerv || !gl.glGetError) { + return measurement; + } + + const GLint advertised = measurement.advertisedMaxVertexAttribs; + // Nothing to bisect, and nothing a clamp could usefully say. + if (advertised < 2) return measurement; + + // THE CONTROL, and the reason a compiler that is simply unavailable cannot be reported as + // this bug: location 0 is the one every ES driver in existence accepts, so a probe that + // cannot compile even that has measured its own failure, not the driver's. + if (!ExplicitVertexInputLocationCompiles(gl, 0, nullptr)) { + MGLOG_I("[driver-bug] %s probe reached no verdict (the location-0 control did not " + "compile, so nothing higher says anything)", + kAttributeLocationProbeName); + return measurement; + } + + // The common case is one compile: a conforming driver takes the highest location it + // advertises and the probe stops there. + if (ExplicitVertexInputLocationCompiles(gl, advertised - 1, nullptr)) return measurement; + + // Bisect for the highest location that still compiles. `low` always compiles (the control + // proved location 0 does) and `high` never does, so the loop closes on the boundary in + // ceil(log2(advertised)) compiles - five for the 32 attributes Adreno advertises. + String rejectionMessage; + ExplicitVertexInputLocationCompiles(gl, advertised - 1, &rejectionMessage); + Int low = 0; + Int high = advertised - 1; + while (high - low > 1) { + const Int middle = low + (high - low) / 2; + if (ExplicitVertexInputLocationCompiles(gl, middle, &rejectionMessage)) { + low = middle; + } else { + high = middle; + } + } + + measurement.detected = true; + measurement.usableLocations = low + 1; + measurement.driverMessage = Move(rejectionMessage); + measurement.bindAttribLocationReachesAdvertisedMax = BindAttribLocationReaches(gl, advertised - 1); + MGLOG_I("[driver-bug] %s probe: GL_MAX_VERTEX_ATTRIBS is %d but layout(location = N) on a " + "vertex input is refused from N = %d upward - only %d location(s) are usable; " + "glBindAttribLocation(%d) %s%s%s", + kAttributeLocationProbeName, advertised, measurement.usableLocations, + measurement.usableLocations, advertised - 1, + measurement.bindAttribLocationReachesAdvertisedMax ? "still resolves correctly" + : "does not resolve either", + measurement.driverMessage.empty() ? "" : "; the driver says: ", + measurement.driverMessage.c_str()); + return measurement; + } + + const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling(const GLESFunctionsTable& gl) { + static const VertexInputLocationCeilingMeasurement measurement = + ProbeExplicitVertexInputLocationCeiling(gl); + return measurement; + } + + namespace { + Optional ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) { + const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl); + if (!measurement.detected) return std::nullopt; + String detail = + format("GL_MAX_VERTEX_ATTRIBS is {} but the ESSL compiler refuses " + "layout(location = N) on a vertex input for every N at or above {}, for float " + "and integer inputs alike - so {} of the {} attributes advertised cannot be " + "declared at all", + measurement.advertisedMaxVertexAttribs, measurement.usableLocations, + measurement.advertisedMaxVertexAttribs - measurement.usableLocations, + measurement.advertisedMaxVertexAttribs); + if (!measurement.driverMessage.empty()) { + detail += format(" - the driver says \"{}\"", measurement.driverMessage); + } + detail += measurement.bindAttribLocationReachesAdvertisedMax + ? format(". The same driver ACCEPTS glBindAttribLocation({}) on an unqualified " + "input and resolves it correctly, so the attributes are there and only " + "the layout qualifier is capped", + measurement.advertisedMaxVertexAttribs - 1) + : ". glBindAttribLocation does not reach those locations either, so the " + "attributes appear genuinely absent rather than merely unspellable"; + detail += format(". MobileGL emits its vertex inputs as layout qualifiers, so it advertises the " + "{} locations it can actually deliver rather than the {} the driver claims. An " + "application asking for more used to be handed a count it could not build a " + "shader against, which failed at the stage compile with no way back", + measurement.usableLocations, measurement.advertisedMaxVertexAttribs); + return DriverBugFinding{"Vertex input layout(location) capped below GL_MAX_VERTEX_ATTRIBS", + DriverBugVerdict::Fixed, Move(detail)}; + } + Optional ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) { if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt; return DriverBugFinding{ @@ -1448,6 +1638,7 @@ namespace MobileGL::MG_Util::SelfTest { &ProbeImageLocationPerNameBug, &ProbeCrossStageImageQualifierMergeBug, &ProbeImageCoherencyResidualBug, + &ProbeExplicitVertexInputLocationCeilingBug, }; } // namespace diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index f3cf4333..4e2de4b9 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -55,6 +55,53 @@ namespace MobileGL::MG_Util::SelfTest { String detail; }; + // What the vertex-input location probe measured. The ceiling is reported rather than + // hard-coded: it is a driver property, and a clamp derived from a number measured on some + // other device is exactly the hard-coded vendor quirk this file exists to avoid. + struct VertexInputLocationCeilingMeasurement { + Bool detected = false; + // GL_MAX_VERTEX_ATTRIBS as the driver answers it. + Int advertisedMaxVertexAttribs = 0; + // How many locations `layout(location = N)` on a vertex input actually accepts, i.e. the + // highest N that compiles plus one. Equal to advertisedMaxVertexAttribs when the driver + // is not affected, and when the probe reached no verdict - so a caller can clamp to it + // unconditionally and an inconclusive probe changes nothing. + Int usableLocations = 0; + // Whether glBindAttribLocation(advertisedMaxVertexAttribs - 1) still links and resolves. + // Only measured when `detected`; see the second control in the .cpp for why it decides + // what the finding is allowed to claim. + Bool bindAttribLocationReachesAdvertisedMax = false; + // The first line of the driver's compile log for a refused declaration, so the report + // quotes the driver rather than paraphrasing it. + String driverMessage; + }; + + // Compiles `layout(location = N) in vec4` on its own at a series of N and finds the highest + // one the driver's ESSL compiler accepts. + // + // Adreno 830 advertises GL_MAX_VERTEX_ATTRIBS = 32 and then refuses the qualifier for every + // N >= 16 ("the location is not within attribute range [0, MAX_ATTRIBUTES-1]"), for float and + // integer inputs alike - so half the attributes it advertises cannot be declared. MobileGL + // emits vertex inputs as layout qualifiers, which makes the advertised count a promise it + // cannot keep; the measured ceiling is what it advertises instead. + // + // TWO CONTROLS. Location 0 must compile, or the probe has measured its own failure rather + // than the driver's. And glBindAttribLocation at the advertised maximum is tried separately, + // because that is what separates "only the layout qualifier is capped" (which is what this + // driver does) from "the attributes are not there at all" - two findings that justify the + // same clamp but very different report text. + // + // Compile-only, and bisected: one shader compile on a conforming driver, about seven on an + // affected one. Returns a measurement with `detected` false and `usableLocations` equal to + // the advertised count when an entry point is missing or a control fails, so an + // inconclusive probe never withdraws anything. + VertexInputLocationCeilingMeasurement ProbeExplicitVertexInputLocationCeiling( + const MG_External::GLESFunctionsTable& gl); + + // ProbeExplicitVertexInputLocationCeiling(), evaluated at most once per process. + const VertexInputLocationCeilingMeasurement& ExplicitVertexInputLocationCeiling( + const MG_External::GLESFunctionsTable& gl); + // Draws one point through VS+GS+FS whose geometry stage writes two storage buffers: one // BEFORE its EmitVertex()/EndPrimitive() and one AFTER. Returns true only when the // before-emit write lands and the after-emit write does not.