mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (DirectGLES): advertise the vertex attributes whose layout(location) the driver's compiler will actually accept
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Loader.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include <Config.h>
|
||||
#include <cmath>
|
||||
@@ -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;
|
||||
|
||||
@@ -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<GLsizei>(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<GLuint>(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<DriverBugFinding> 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<DriverBugFinding> 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user