mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (MG_Backend/DirectGLES): rebase gl_InstanceID for native indirect draws on ANGLE
ES keeps gl_InstanceID zero-based and ignores the indirect command's 'reserved, must be zero' word, but ANGLE-on-Vulkan forwards the command verbatim to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V InstanceIndex, which includes firstInstance. Shaders computing gl_BaseInstance + gl_InstanceID (Flywheel indirect) then add the base twice, scrambling instance-to-mesh association. Probe the actual driver semantics at capability-fill time with a tiny indirect draw (an ES indirect draw needs a non-default VAO) and, on leaking drivers, rewrite vertex shaders that use the native indirect SSBO machinery so gl_InstanceID subtracts the command's baseInstance word during native indirect draws. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1078,8 +1078,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// shader emulation. Without GL_EXT_base_instance a non-zero baseInstance in the command is
|
||||
// technically undefined in ES; mobile drivers ignore the reserved word, instanced-array
|
||||
// fetches were never baseInstance-offset here anyway, and the CPU fallback cannot see
|
||||
// GPU-written command fields at all - so native is never worse. Only client-memory
|
||||
// commands take the CPU per-command loop.
|
||||
// GPU-written command fields at all - so native is never worse. ANGLE-on-Vulkan instead
|
||||
// leaks the word into gl_InstanceID (it becomes vkCmdDrawIndexedIndirect's firstInstance);
|
||||
// the shader rewrite compensates by rebasing gl_InstanceID during these draws when
|
||||
// IndirectDrawInstanceIdIncludesBaseInstance is set (PromoteDrawParameterGlobalsToUniforms).
|
||||
// Only client-memory commands take the CPU per-command loop.
|
||||
static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes,
|
||||
SizeT commandOffset,
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
constexpr const char* BASE_INSTANCE_LOWERED_NAME = "mg_BaseInstanceLowered";
|
||||
constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex";
|
||||
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
|
||||
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
|
||||
|
||||
static Bool IsAngleLlvmpipeRenderer() {
|
||||
return g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos &&
|
||||
@@ -157,10 +158,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int "}) {
|
||||
const String declaration = String(declPrefix) + BASE_INSTANCE_LOWERED_NAME + ";";
|
||||
const SizeT pos = source.find(declaration);
|
||||
SizeT pos = source.find(declaration);
|
||||
if (pos == String::npos) {
|
||||
continue;
|
||||
}
|
||||
// On drivers where native indirect draws leak the command's baseInstance into
|
||||
// gl_InstanceID (ANGLE-on-Vulkan; IndirectDrawInstanceIdIncludesBaseInstance),
|
||||
// rebase gl_InstanceID back to zero during those draws so shaders computing
|
||||
// gl_BaseInstance + gl_InstanceID don't add the base twice. Scoped to shaders
|
||||
// using gl_BaseInstance: only they take the native indirect SSBO machinery.
|
||||
const Bool rebaseInstanceId = g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance &&
|
||||
source.find("gl_InstanceID") != String::npos;
|
||||
if (rebaseInstanceId) {
|
||||
source = ReplaceIdentifier(source, "gl_InstanceID", ZERO_BASED_INSTANCE_ID_NAME);
|
||||
pos = source.find(declaration); // the declaration contains no gl_InstanceID
|
||||
}
|
||||
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
|
||||
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
|
||||
: 0;
|
||||
@@ -172,6 +184,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
|
||||
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
|
||||
" { highp uint mg_indirectWords[]; };\n";
|
||||
if (rebaseInstanceId) {
|
||||
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " (gl_InstanceID - ((" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : 0))\n";
|
||||
}
|
||||
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " ((" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + " >= 0) ? int(mg_indirectWords[uint(" +
|
||||
BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ")]) : " + BASE_INSTANCE_UNIFORM_NAME + ")";
|
||||
|
||||
@@ -543,6 +543,155 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// Detects whether indirect draws leak the command's baseInstance word ("reserved, must
|
||||
// be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers keep
|
||||
// gl_InstanceID zero-based, but ANGLE's Vulkan backend forwards the command verbatim to
|
||||
// vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V InstanceIndex, which includes
|
||||
// firstInstance. The DirectGLES native indirect-draw path uses this answer to keep
|
||||
// gl_InstanceID zero-based in rewritten shaders (PromoteDrawParameterGlobalsToUniforms).
|
||||
static Bool ProbeIndirectInstanceIdIncludesBaseInstance(const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& f) {
|
||||
const Bool esVersionOk =
|
||||
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||
if (!esVersionOk || !f.glDrawArraysIndirect || !f.glBindBufferBase || !f.glMapBufferRange ||
|
||||
!f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
|
||||
return false;
|
||||
}
|
||||
GLint maxVertexSsboBlocks = 0;
|
||||
f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
|
||||
if (maxVertexSsboBlocks < 1) {
|
||||
// The native indirect machinery cannot read the command buffer from the vertex
|
||||
// stage on this driver anyway; assume conforming zero-based gl_InstanceID.
|
||||
MGLOG_I("baseInstance probe skipped: GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d", maxVertexSsboBlocks);
|
||||
return false;
|
||||
}
|
||||
while (f.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
const char* vsSource = "#version 310 es\n"
|
||||
"layout(std430, binding = 0) buffer MgProbeResult { highp int mg_probeValue; };\n"
|
||||
"void main() {\n"
|
||||
" mg_probeValue = gl_InstanceID;\n"
|
||||
" gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n"
|
||||
" gl_PointSize = 1.0;\n"
|
||||
"}\n";
|
||||
const char* fsSource = "#version 310 es\n"
|
||||
"void main() {}\n";
|
||||
const auto compileShader = [&f](GLenum type, const char* src) -> GLuint {
|
||||
const GLuint shader = f.glCreateShader(type);
|
||||
if (shader == 0) {
|
||||
return 0;
|
||||
}
|
||||
f.glShaderSource(shader, 1, &src, nullptr);
|
||||
f.glCompileShader(shader);
|
||||
GLint status = GL_FALSE;
|
||||
f.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (status != GL_TRUE) {
|
||||
f.glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
const GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource);
|
||||
const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource);
|
||||
GLuint program = 0;
|
||||
if (vs != 0 && fs != 0) {
|
||||
program = f.glCreateProgram();
|
||||
if (program != 0) {
|
||||
f.glAttachShader(program, vs);
|
||||
f.glAttachShader(program, fs);
|
||||
f.glLinkProgram(program);
|
||||
GLint status = GL_FALSE;
|
||||
f.glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status != GL_TRUE) {
|
||||
f.glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vs != 0) f.glDeleteShader(vs);
|
||||
if (fs != 0) f.glDeleteShader(fs);
|
||||
if (program == 0) {
|
||||
MGLOG_I("baseInstance probe skipped: probe program failed to build (vs=%u fs=%u)", vs, fs);
|
||||
while (f.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr GLuint kProbeBaseInstance = 7;
|
||||
struct {
|
||||
GLuint count;
|
||||
GLuint instanceCount;
|
||||
GLuint first;
|
||||
GLuint baseInstance;
|
||||
} command = {1, 1, 0, kProbeBaseInstance};
|
||||
const GLint sentinel = -1;
|
||||
// ES makes indirect draws INVALID_OPERATION on the default vertex array object.
|
||||
GLuint vao = 0;
|
||||
f.glGenVertexArrays(1, &vao);
|
||||
f.glBindVertexArray(vao);
|
||||
GLuint buffers[2] = {0, 0}; // [0] = result SSBO, [1] = indirect command buffer
|
||||
f.glGenBuffers(2, buffers);
|
||||
f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[0]);
|
||||
f.glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(sentinel), &sentinel, GL_STATIC_DRAW);
|
||||
f.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffers[0]);
|
||||
f.glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffers[1]);
|
||||
f.glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(command), &command, GL_STATIC_DRAW);
|
||||
|
||||
// The default framebuffer may be incomplete (e.g. surfaceless contexts) and draws
|
||||
// are validated against completeness even under GL_RASTERIZER_DISCARD, so give the
|
||||
// probe its own 1x1 target.
|
||||
GLuint framebuffer = 0;
|
||||
GLuint renderbuffer = 0;
|
||||
f.glGenFramebuffers(1, &framebuffer);
|
||||
f.glGenRenderbuffers(1, &renderbuffer);
|
||||
f.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
f.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
|
||||
f.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
f.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
|
||||
f.glUseProgram(program);
|
||||
f.glEnable(GL_RASTERIZER_DISCARD);
|
||||
f.glDrawArraysIndirect(GL_POINTS, nullptr);
|
||||
f.glDisable(GL_RASTERIZER_DISCARD);
|
||||
f.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
|
||||
Bool includesBase = false;
|
||||
const GLenum drawError = f.glGetError();
|
||||
if (drawError == GL_NO_ERROR) {
|
||||
f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[0]);
|
||||
const void* mapped = f.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLint), GL_MAP_READ_BIT);
|
||||
if (mapped != nullptr) {
|
||||
GLint written = -1;
|
||||
std::memcpy(&written, mapped, sizeof(written));
|
||||
f.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
|
||||
includesBase = written == static_cast<GLint>(kProbeBaseInstance);
|
||||
MGLOG_I("baseInstance probe: shader observed gl_InstanceID = %d (baseInstance was %u)", written,
|
||||
kProbeBaseInstance);
|
||||
} else {
|
||||
MGLOG_I("baseInstance probe inconclusive: result map failed");
|
||||
}
|
||||
} else {
|
||||
MGLOG_I("baseInstance probe inconclusive: draw raised GL error 0x%x", drawError);
|
||||
}
|
||||
|
||||
f.glUseProgram(0);
|
||||
f.glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
f.glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
f.glDeleteFramebuffers(1, &framebuffer);
|
||||
f.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
f.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
f.glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||
f.glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
|
||||
f.glBindVertexArray(0);
|
||||
f.glDeleteVertexArrays(1, &vao);
|
||||
f.glDeleteBuffers(2, buffers);
|
||||
f.glDeleteProgram(program);
|
||||
while (f.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return includesBase;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -757,6 +906,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMax);
|
||||
MGLOG_I(" GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
|
||||
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance =
|
||||
ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
|
||||
MGLOG_I(" Indirect draw gl_InstanceID includes baseInstance: %s",
|
||||
caps.IndirectDrawInstanceIdIncludesBaseInstance ? "true" : "false");
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Util::BackendLoader
|
||||
|
||||
@@ -1019,6 +1019,12 @@ namespace MobileGL {
|
||||
Bool SupportsPersistentMapping = false;
|
||||
Bool SupportsNorm16Texture = false;
|
||||
Bool SupportsBaseInstance = false;
|
||||
// True when indirect draws leak the command's baseInstance word ("reserved,
|
||||
// must be zero" in unextended ES) into gl_InstanceID. Conforming ES drivers
|
||||
// keep gl_InstanceID zero-based; ANGLE's Vulkan backend hands the command
|
||||
// straight to vkCmdDraw*Indirect and compiles gl_InstanceID to SPIR-V
|
||||
// InstanceIndex, which includes firstInstance.
|
||||
Bool IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
Float AliasedLineWidthRangeMin = 1.0f;
|
||||
Float AliasedLineWidthRangeMax = 1.0f;
|
||||
|
||||
Reference in New Issue
Block a user