[Feat] (MG_Backend/DirectGLES): support Flywheel indirect rendering

- Advertise ARB_gpu_shader5 / ARB_multi_bind / ARB_shading_language_420pack /
  ARB_vertex_attrib_binding / ARB_shader_image_size so LWJGL reports
  SUPPORTS_INDIRECT.
- New LowerDrawParametersPass demotes DrawIndex/BaseInstance/BaseVertex
  builtins to Private globals (mg_DrawID/mg_BaseInstance/mg_BaseVertex) for
  the ESSL transpile; SPIRV-Cross otherwise throws for ES profiles. The
  program manager promotes the emitted globals to uniforms and feeds them
  per (sub-)draw.
- Indirect draws now execute natively on the GPU (glDrawElementsIndirect /
  glDrawArraysIndirect per command) when an indirect buffer is bound, so
  compute-written command fields (Flywheel culling updates instanceCount)
  are honored; detects GL_EXT_base_instance and falls back to the CPU loop
  when the command's baseInstance cannot be consumed natively.
- Sync SSBO binding points for graphics draws, not just compute (Flywheel
  vertex shaders read instance data from SSBOs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 13:33:05 +00:00
co-authored by Claude Fable 5
parent 9fbb708e64
commit 5ea49f3c50
11 changed files with 417 additions and 63 deletions
+1
View File
@@ -187,6 +187,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -788,7 +788,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters},
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
E_GL_ARB_shader_image_size},
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
+139 -61
View File
@@ -276,6 +276,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER);
// Graphics shaders may also read SSBOs (e.g. Flywheel's indirect vertex shaders pull
// instance data from storage buffers), so keep those binding points in sync for draws
// and not just for compute dispatches.
SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER);
}
void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) {
@@ -1015,6 +1019,121 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
void SetCurrentDrawID(Uint32 drawId) {
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
if (!currentProgram || !currentProgram->GetLinkStatus()) {
return;
}
const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get());
if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) {
backendProgramIt->second->SetDrawID(drawId);
}
}
static Bool SupportsNativeIndirectDraws() {
const auto& version = g_GLESCapabilities.GLESVersion;
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
g_GLESFuncs.glDrawArraysIndirect != nullptr;
}
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
// for the per-command baseInstance, which is CPU-authored, to feed the mg_BaseInstance
// shader emulation. Falls back to the CPU per-command loop for client-memory commands or
// when the driver cannot consume the command's baseInstance field (no GL_EXT_base_instance).
static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes,
SizeT commandOffset, Bool hasIndirectBuffer, GLsizei drawcount,
GLsizei stride, const char* label) {
Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws();
if (useNative && !g_GLESCapabilities.SupportsBaseInstance) {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.baseInstance != 0) {
useNative = false;
MGLOG_W("%s: non-zero baseInstance without GL_EXT_base_instance, falling back to CPU "
"emulation (GPU-written command fields will not be honored)",
label);
break;
}
}
}
if (useNative) {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawElementsIndirect(
mode, type, reinterpret_cast<const void*>(commandOffset + static_cast<SizeT>(i) * stride));
}
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
continue;
}
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
}
}
SetCurrentDrawID(0);
SetCurrentBaseInstance(0);
}
static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset,
Bool hasIndirectBuffer, GLsizei drawcount, GLsizei stride,
const char* label) {
Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws();
if (useNative && !g_GLESCapabilities.SupportsBaseInstance) {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.baseInstance != 0) {
useNative = false;
MGLOG_W("%s: non-zero baseInstance without GL_EXT_base_instance, falling back to CPU "
"emulation (GPU-written command fields will not be honored)",
label);
break;
}
}
}
if (useNative) {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawArraysIndirect(
mode, reinterpret_cast<const void*>(commandOffset + static_cast<SizeT>(i) * stride));
}
} else {
for (GLsizei i = 0; i < drawcount; ++i) {
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
continue;
}
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount));
}
}
SetCurrentDrawID(0);
SetCurrentBaseInstance(0);
}
void PrepareForCompute(Bool includeDispatchIndirectBuffer) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1170,19 +1289,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
for (GLsizei i = 0; i < drawcount; ++i) {
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
continue;
}
SetCurrentBaseInstance(cmd.baseInstance);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
}
SetCurrentBaseInstance(0);
const Bool hasIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
hasIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect");
}
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
@@ -1242,19 +1352,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 actualDrawCount = 0;
std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount));
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
for (Uint32 i = 0; i < actualDrawCount; ++i) {
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, drawData->data() + commandOffset + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
continue;
}
SetCurrentBaseInstance(cmd.baseInstance);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
}
SetCurrentBaseInstance(0);
ExecuteIndexedIndirectCommands(mode, type, indexSize, drawData->data() + commandOffset, commandOffset,
/*hasIndirectBuffer=*/true, static_cast<GLsizei>(actualDrawCount), stride,
"MultiDrawElementsIndirectCount");
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
@@ -1284,18 +1384,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
for (GLsizei i = 0; i < drawcount; ++i) {
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes + static_cast<SizeT>(i) * stride, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
continue;
}
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawArraysInstanced(
mode, static_cast<GLint>(cmd.first), static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount));
}
SetCurrentBaseInstance(0);
const Bool hasIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), hasIndirectBuffer,
drawcount, stride, "MultiDrawArraysIndirect");
}
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
@@ -1358,18 +1450,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
return;
}
SetCurrentBaseInstance(cmd.baseInstance);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex);
SetCurrentBaseInstance(0);
const Bool hasIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast<SizeT>(indirect),
hasIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand),
"DrawElementsIndirect");
}
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
@@ -1397,17 +1482,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, commandBytes, sizeof(cmd));
if (cmd.count == 0 || cmd.instanceCount == 0) {
return;
}
SetCurrentBaseInstance(cmd.baseInstance);
g_GLESFuncs.glDrawArraysInstanced(
mode, static_cast<GLint>(cmd.first), static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount));
SetCurrentBaseInstance(0);
const Bool hasIndirectBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr;
ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast<SizeT>(indirect), hasIndirectBuffer, 1,
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
+53 -1
View File
@@ -9,6 +9,7 @@
#include "Managers.h"
#include "Utils.h"
#include "DirectGLES.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -31,6 +32,8 @@
namespace MobileGL::MG_Backend::DirectGLES {
constexpr Bool PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC = false;
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex";
static Bool IsAngleLlvmpipeRenderer() {
return g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos &&
@@ -113,6 +116,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";");
}
// The LowerDrawParametersPass demotes gl_DrawID / gl_BaseInstance / gl_BaseVertex to plain
// Private globals named mg_DrawID / mg_BaseInstance / mg_BaseVertex; SPIRV-Cross then emits
// them as ordinary global declarations. Turn those declarations into uniforms so the draw
// paths can feed real values per (sub-)draw.
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType) {
if (shaderType != GL_VERTEX_SHADER) {
return source;
}
for (const char* name : {DRAW_ID_UNIFORM_NAME, BASE_INSTANCE_UNIFORM_NAME, BASE_VERTEX_UNIFORM_NAME}) {
for (const char* declPrefix : {"highp int ", "mediump int ", "lowp int ", "int ", "highp uint ",
"mediump uint ", "uint "}) {
const String declaration = String(declPrefix) + name + ";";
const SizeT pos = source.find(declaration);
if (pos == String::npos) {
continue;
}
// Only promote a standalone global declaration, not a uniform we already emitted.
const Bool alreadyUniform = pos >= 8 && source.compare(pos - 8, 8, "uniform ") == 0;
if (!alreadyUniform) {
const Bool hasPrecision = std::strncmp(declPrefix, "int ", 4) != 0 &&
std::strncmp(declPrefix, "uint ", 5) != 0;
const String qualifier = hasPrecision ? "uniform " : "uniform highp ";
source.replace(pos, declaration.size(), qualifier + declaration);
}
break;
}
}
return source;
}
namespace BufferImpl {
namespace {
using MG_State::GLState::BackendBufferResource;
@@ -1919,7 +1952,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
String source;
auto& spirvCode = shaderSpirvs[index];
MG_Util::ShaderTranspiler::SpvcSession spvcSession(spirvCode,
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
// plain globals (mg_*) before handing the module to SPIRV-Cross.
Vector<unsigned int> loweredSpirv;
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
if (glShaderType == GL_VERTEX_SHADER &&
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv) &&
!loweredSpirv.empty()) {
effectiveSpirv = &loweredSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
@@ -1950,6 +1993,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
source = ForceSupporterOutput(source);
source = ClampNormFallbackOutputs(std::move(source), glShaderType,
m_snormFallbackClampOutputMask,
@@ -2005,6 +2049,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
BASE_INSTANCE_UNIFORM_NAME);
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
// Create global UBO
if (stateProgramObject->GetUBOSize() > 0) {
@@ -2034,6 +2079,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast<GLint>(baseInstance));
}
void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const {
if (m_drawIdUniformLocation < 0) {
return;
}
g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast<GLint>(drawId));
}
} // namespace PrgramImpl
namespace SamplerImpl {
@@ -16,6 +16,7 @@
namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
template <typename StateObject, typename BackendObject>
class StateBackendObjectRegistry {
@@ -316,6 +317,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
void Use() const;
void SetBaseInstance(Uint32 baseInstance) const;
void SetDrawID(Uint32 drawId) const;
Uint GetBackendProgramId() const { return m_backendProgramId; }
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
@@ -325,6 +327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint m_backendProgramId = 0;
Uint m_backendGlobalUBOId = 0;
Int m_baseInstanceUniformLocation = -1;
Int m_drawIdUniformLocation = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
Bool m_isInitialized = false;
@@ -574,6 +574,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
caps.SupportsNorm16Texture = true;
}
if (std::strcmp(extension, "GL_EXT_base_instance") == 0) {
caps.SupportsBaseInstance = true;
}
}
}
@@ -1018,6 +1018,7 @@ namespace MobileGL {
String GLESShadingLanguageVersionString;
Bool SupportsPersistentMapping = false;
Bool SupportsNorm16Texture = false;
Bool SupportsBaseInstance = false;
Int UniformBufferOffsetAlignment = 256;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
@@ -12,6 +12,7 @@
#include "SpirvPasses/FlattenInterfaceStructPass.h"
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
@@ -251,6 +252,18 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options;
session.CreateOptions(&options);
@@ -22,6 +22,11 @@ namespace MobileGL {
static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
// (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
static Result<String> DecompileShader(SpvcSession& session);
};
} // namespace ShaderTranspiler
@@ -0,0 +1,162 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "LowerDrawParametersPass.h"
#include "spirv.hpp"
#include "source/opt/def_use_manager.h"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/opt/module.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
const char* LoweredNameForBuiltin(spv::BuiltIn builtin) {
switch (builtin) {
case spv::BuiltIn::BaseVertex:
return "mg_BaseVertex";
case spv::BuiltIn::BaseInstance:
return "mg_BaseInstance";
case spv::BuiltIn::DrawIndex:
return "mg_DrawID";
default:
return nullptr;
}
}
void ReplaceName(IRContext* context, uint32_t id, const char* name) {
for (auto& debugInst : context->debugs2()) {
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id) {
debugInst.SetInOperand(
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(name));
return;
}
}
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpName, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {id}},
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
}
void RemoveFromEntryPointInterfaces(IRContext* context, uint32_t id) {
for (Instruction& entryPoint : context->module()->entry_points()) {
std::vector<Operand> newOperands;
Bool changed = false;
for (uint32_t i = 0; i < entryPoint.NumInOperands(); ++i) {
const Operand& operand = entryPoint.GetInOperand(i);
// Interface ids start after execution model, entry-point id and name.
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
entryPoint.GetSingleWordInOperand(i) == id) {
changed = true;
continue;
}
newOperands.push_back(operand);
}
if (changed) {
entryPoint.SetInOperands(std::move(newOperands));
}
}
}
} // namespace
spvtools::opt::Pass::Status LowerDrawParametersPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
// Collect the BuiltIn decorations we want to lower first; mutating while
// iterating annotations invalidates the range.
struct LoweredVariable {
Instruction* variable = nullptr;
Instruction* decoration = nullptr;
const char* name = nullptr;
};
std::vector<LoweredVariable> targets;
for (auto& annotation : irContext->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate ||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
const auto builtin = static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2));
const char* loweredName = LoweredNameForBuiltin(builtin);
if (loweredName == nullptr) {
continue;
}
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
continue;
}
targets.push_back({variable, &annotation, loweredName});
}
if (targets.empty()) {
return Status::SuccessWithoutChange;
}
auto* typeMgr = irContext->get_type_mgr();
for (auto& target : targets) {
Instruction* variable = target.variable;
const uint32_t variableId = variable->result_id();
// Demote the Input builtin to a plain Private global.
Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
const uint32_t pointeeTypeId = pointerType->GetSingleWordInOperand(1);
const uint32_t privatePointerTypeId =
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private);
variable->SetResultType(privatePointerTypeId);
variable->SetInOperand(0, {static_cast<uint32_t>(spv::StorageClass::Private)});
irContext->KillInst(target.decoration);
RemoveFromEntryPointInterfaces(irContext, variableId);
ReplaceName(irContext, variableId, target.name);
}
// The DrawParameters capability only covered these builtins; it must not leak
// into the ESSL decompile.
std::vector<Instruction*> deadModuleInsts;
for (auto& capability : irContext->module()->capabilities()) {
if (static_cast<spv::Capability>(capability.GetSingleWordInOperand(0)) ==
spv::Capability::DrawParameters) {
deadModuleInsts.push_back(&capability);
}
}
for (auto& extension : irContext->module()->extensions()) {
if (extension.GetInOperand(0).AsString() == "SPV_KHR_shader_draw_parameters") {
deadModuleInsts.push_back(&extension);
}
}
for (auto* inst : deadModuleInsts) {
irContext->KillInst(inst);
}
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken LowerDrawParametersPass::CreateLowerDrawParametersPass() {
return spvtools::Optimizer::PassToken(MakeUnique<LowerDrawParametersPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// ESSL has no gl_DrawID / gl_BaseInstance / gl_BaseVertex builtins and SPIRV-Cross
// refuses to emit them for ES targets. This pass demotes the DrawIndex /
// BaseInstance / BaseVertex builtin inputs to plain Private globals with
// well-known names (mg_DrawID / mg_BaseInstance / mg_BaseVertex) so the decompiled
// ESSL declares ordinary globals; the DirectGLES program manager then upgrades the
// declarations to uniforms and feeds them per (sub-)draw. Only meant for the
// DirectGLES transpile path - the Vulkan backend keeps the native builtins.
class LowerDrawParametersPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "lower-draw-parameters"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateLowerDrawParametersPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL