[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:
2026-07-09 12:14:02 +00:00
co-authored by Claude Fable 5
parent a6a5edf573
commit 85b68a9969
4 changed files with 183 additions and 3 deletions
+18 -1
View File
@@ -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 + ")";