diff --git a/CMakeLists.txt b/CMakeLists.txt index db53094c..84fde986 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index d41aa01b..93a335b7 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.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 diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5f7e27fc..72607ebb 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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(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(i) * stride, sizeof(cmd)); + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + g_GLESFuncs.glDrawElementsIndirect( + mode, type, reinterpret_cast(commandOffset + static_cast(i) * stride)); + } + } else { + for (GLsizei i = 0; i < drawcount; ++i) { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + if (cmd.count == 0 || cmd.instanceCount == 0) { + continue; + } + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; + g_GLESFuncs.glDrawElementsInstancedBaseVertex( + mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), + static_cast(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(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(i) * stride, sizeof(cmd)); + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + g_GLESFuncs.glDrawArraysIndirect( + mode, reinterpret_cast(commandOffset + static_cast(i) * stride)); + } + } else { + for (GLsizei i = 0; i < drawcount; ++i) { + DrawArraysIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + if (cmd.count == 0 || cmd.instanceCount == 0) { + continue; + } + SetCurrentDrawID(static_cast(i)); + SetCurrentBaseInstance(cmd.baseInstance); + g_GLESFuncs.glDrawArraysInstanced(mode, static_cast(cmd.first), + static_cast(cmd.count), + static_cast(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(i) * stride, sizeof(cmd)); - if (cmd.count == 0 || cmd.instanceCount == 0) { - continue; - } - SetCurrentBaseInstance(cmd.baseInstance); - const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; - g_GLESFuncs.glDrawElementsInstancedBaseVertex( - mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), - static_cast(cmd.instanceCount), cmd.baseVertex); - } - SetCurrentBaseInstance(0); + const Bool hasIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(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(actualDrawCount, static_cast(maxdrawcount)); - for (Uint32 i = 0; i < actualDrawCount; ++i) { - DrawElementsIndirectCommand cmd{}; - std::memcpy(&cmd, drawData->data() + commandOffset + static_cast(i) * stride, sizeof(cmd)); - if (cmd.count == 0 || cmd.instanceCount == 0) { - continue; - } - SetCurrentBaseInstance(cmd.baseInstance); - const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; - g_GLESFuncs.glDrawElementsInstancedBaseVertex( - mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), - static_cast(cmd.instanceCount), cmd.baseVertex); - } - SetCurrentBaseInstance(0); + ExecuteIndexedIndirectCommands(mode, type, indexSize, drawData->data() + commandOffset, commandOffset, + /*hasIndirectBuffer=*/true, static_cast(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(i) * stride, sizeof(cmd)); - if (cmd.count == 0 || cmd.instanceCount == 0) { - continue; - } - SetCurrentBaseInstance(cmd.baseInstance); - g_GLESFuncs.glDrawArraysInstanced( - mode, static_cast(cmd.first), static_cast(cmd.count), - static_cast(cmd.instanceCount)); - } - SetCurrentBaseInstance(0); + const Bool hasIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(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(cmd.firstIndex) * indexSize; - g_GLESFuncs.glDrawElementsInstancedBaseVertex( - mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), - static_cast(cmd.instanceCount), cmd.baseVertex); - SetCurrentBaseInstance(0); + const Bool hasIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(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(cmd.first), static_cast(cmd.count), - static_cast(cmd.instanceCount)); - SetCurrentBaseInstance(0); + const Bool hasIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), hasIndirectBuffer, 1, + sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); } void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 3f041430..97f053df 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -9,6 +9,7 @@ #include "Managers.h" #include "Utils.h" #include "DirectGLES.h" +#include #include #include @@ -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 loweredSpirv; + const Vector* 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(baseInstance)); } + + void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const { + if (m_drawIdUniformLocation < 0) { + return; + } + g_GLESFuncs.glUniform1i(m_drawIdUniformLocation, static_cast(drawId)); + } } // namespace PrgramImpl namespace SamplerImpl { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 23235ba1..d59611c9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -16,6 +16,7 @@ namespace MobileGL::MG_Backend::DirectGLES { String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); + String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType); template class StateBackendObjectRegistry { @@ -316,6 +317,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncToBackend(const SharedPtr& 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; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index e5747bb9..717827c5 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -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; + } } } diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 93b00bb9..62a66895 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -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; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index da19fab9..facc18ce 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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& inputBinary, + Vector& 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 ShaderCompiler::DecompileShader(SpvcSession& session) { spvc_compiler_options options; session.CreateOptions(&options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 2908a283..2d5a0f75 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -22,6 +22,11 @@ namespace MobileGL { static Result>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib); static bool SanitizeAndOptimizeBinary(const Vector& inputBinary, Vector& 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& inputBinary, + Vector& outputBinary); static Result DecompileShader(SpvcSession& session); }; } // namespace ShaderTranspiler diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp new file mode 100644 index 00000000..24270ad8 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp @@ -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 + +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(name)); + return; + } + } + context->AddDebug2Inst(spvtools::MakeUnique( + context, spv::Op::OpName, 0, 0, + std::initializer_list{ + {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 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 targets; + + for (auto& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate || + static_cast(annotation.GetSingleWordInOperand(1)) != + spv::Decoration::BuiltIn) { + continue; + } + + const auto builtin = static_cast(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(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(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 deadModuleInsts; + for (auto& capability : irContext->module()->capabilities()) { + if (static_cast(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()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.h new file mode 100644 index 00000000..3464ae61 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.h @@ -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 + +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