diff --git a/CMakeLists.txt b/CMakeLists.txt index 84fde986..607efeb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 5cddaa23..1bf479d8 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -72,6 +72,10 @@ namespace MobileGL { SizeT GetRenderbufferFormatCapabilityTargetIndex(); void PrintFormatCapabilities(const FormatCapabilityCache& cache); + // Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync + // and released by GLFunctionsTable::DeleteSync. + using BackendSyncHandle = void*; + struct GLFunctionsTable { void (*DrawArrays)(GLenum mode, GLint first, GLsizei count); void (*DrawElements)(GLenum mode, GLsizei count, GLenum type, const void* indices); @@ -157,6 +161,16 @@ namespace MobileGL { GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name); GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name); void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); + // GL fence sync objects. All entries are optional (may be null); the + // frontend then falls back to always-signaled sync semantics. + // FenceSync may itself return null when the backend cannot create a + // fence right now (e.g. the calling thread does not own the backend + // context); the frontend treats such a sync as always signaled. + BackendSyncHandle (*FenceSync)(); + GLenum (*ClientWaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void (*WaitSync)(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void (*DeleteSync)(BackendSyncHandle sync); + Bool (*GetSyncStatus)(BackendSyncHandle sync); // true = signaled }; struct GlobalBackendFunctionsTable { GLFunctionsTable GL; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 93a335b7..a6e086fe 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -866,6 +866,11 @@ namespace MobileGL::MG_Backend::DirectGLES { funcsTable.GL.GenerateMipmap = GenerateMipmap; funcsTable.GL.ReadPixels = ReadPixels; funcsTable.GL.GetTexImage = GetTexImage; + funcsTable.GL.FenceSync = FenceSync; + funcsTable.GL.ClientWaitSync = ClientWaitSync; + funcsTable.GL.WaitSync = WaitSync; + funcsTable.GL.DeleteSync = DeleteSync; + funcsTable.GL.GetSyncStatus = GetSyncStatus; funcsTableInitialized = true; } return funcsTable; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d310a5e6..4aa167d6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -25,9 +25,11 @@ #include #include #include +#include #include #include #include +#include #if defined(__linux__) && !defined(__ANDROID__) && __has_include() #pragma push_macro("Bool") #pragma push_macro("None") @@ -403,7 +405,11 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - for (Uint unit = 0; unit < MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; ++unit) { + // The frontend tracks more image units than ES exposes; binding past the device + // limit raises GL_INVALID_VALUE on every dispatch. + const Uint unitCount = std::min(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS, + static_cast(std::max(g_GLESCapabilities.MaxImageUnits, 0))); + for (Uint unit = 0; unit < unitCount; ++unit) { SyncImageTextureBinding(unit); } } @@ -818,6 +824,35 @@ namespace MobileGL::MG_Backend::DirectGLES { FramebufferImpl::g_fboBindVersions[(SizeT)target] = slot.GetVersion(); } + static void BindCurrentProgramWithResources(); + static void BindCurrentTextures(); + + // Image uniforms take their unit from the layout(binding=N) qualifier baked into + // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be + // assigned through glUniform1i. + static Bool IsImageUniformType(GLenum type) { + switch (type) { + case 0x904D: /*GL_IMAGE_2D*/ + case 0x904E: /*GL_IMAGE_3D*/ + case 0x9050: /*GL_IMAGE_CUBE*/ + case 0x9051: /*GL_IMAGE_BUFFER*/ + case 0x9053: /*GL_IMAGE_2D_ARRAY*/ + case 0x9058: /*GL_INT_IMAGE_2D*/ + case 0x9059: /*GL_INT_IMAGE_3D*/ + case 0x905B: /*GL_INT_IMAGE_CUBE*/ + case 0x905C: /*GL_INT_IMAGE_BUFFER*/ + case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/ + case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/ + case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/ + case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/ + case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/ + case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/ + return true; + default: + return false; + } + } + void PrepareForDraw(DrawSyncBit syncBit) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -846,45 +881,63 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - { + BindCurrentTextures(); + BindCurrentProgramWithResources(); + } + + // Rebinds every frontend texture unit's textures (and sampler objects) on the + // backend context. Needed before draws AND compute dispatches: content syncs + // (SyncTextureObjectToBackend) bind scratch textures on the active unit as a + // side effect, so unit bindings must be re-established afterwards or shaders + // sample whatever texture the last sync left behind (e.g. Flywheel's depth + // pyramid downsample reading a stale unit-0 binding instead of the depth + // attachment). + static void BindCurrentTextures() { #ifdef TRACY_ENABLE - ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); + ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND); #endif - Int maxTextureUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; - for (Int unit = 0; unit < maxTextureUnits; ++unit) { - auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); + Int maxTextureUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; + for (Int unit = 0; unit < maxTextureUnits; ++unit) { + auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); - for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) { - const auto& textureObject = bindingSlot.GetBoundObject(); - if (!textureObject) continue; + for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) { + const auto& textureObject = bindingSlot.GetBoundObject(); + if (!textureObject) continue; - // Bind texture object - auto target = textureObject->GetTarget(); - if (!TextureImpl::IsSupportedTextureTarget(target)) { - MGLOG_D(" Texture target %s is not supported, skipping.", - MG_Util::ConvertTextureTargetToString(target).c_str()); - continue; - } - const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); - if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; + // Bind texture object + auto target = textureObject->GetTarget(); + if (!TextureImpl::IsSupportedTextureTarget(target)) { + MGLOG_D(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(target).c_str()); + continue; + } + const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); + if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; - GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target); - backendTextureIt->second->Bind(targetGL, unit); + GLenum targetGL = MG_Util::ConvertTextureTargetToGLEnum(target); + backendTextureIt->second->Bind(targetGL, unit); + } + + // Bind sampler object if necessary + const auto& samplerObject = textureUnit.GetSamplerObject(); + if (samplerObject) { + const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get()); + if (backendSamplerIt != SamplerImpl::g_backendSamplerObjects.end()) { + backendSamplerIt->second->Bind(unit); } - // Bind sampler object if necessary - const auto& samplerObject = textureUnit.GetSamplerObject(); - if (samplerObject) { - const auto& backendSamplerIt = SamplerImpl::g_backendSamplerObjects.find(samplerObject.get()); - if (backendSamplerIt != SamplerImpl::g_backendSamplerObjects.end()) { - backendSamplerIt->second->Bind(unit); - } - - } else { - } + } else { } } + } + // Binds the current program's backend object and re-establishes its per-program + // resources: global UBO contents, uniform-block bindings, and sampler uniform + // units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the + // association must be rebuilt through the API). Compute dispatches depend on + // this as much as draws do — e.g. Flywheel's cull shader reads the + // _FlwFrameUniforms block and the _flw_depthPyramid sampler. + static void BindCurrentProgramWithResources() { const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram(); if (currentProgram && currentProgram->GetLinkStatus()) { #ifdef TRACY_ENABLE @@ -933,6 +986,12 @@ namespace MobileGL::MG_Backend::DirectGLES { auto binding = currentProgram->GetUniformBlockBinding(i); auto& name = currentProgram->GetUniformBlockName(i); GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(backendProgramId, name.c_str()); + if (backendBlkIdx == GL_INVALID_INDEX) { + // Not a uniform block in the backend program: either eliminated as + // unused, or an SSBO block (the frontend's reflection lists those + // among uniform blocks); SSBO bindings are baked into the ESSL. + continue; + } g_GLESFuncs.glUniformBlockBinding(backendProgramId, backendBlkIdx, lastUBOBinding); // Connect buffer to backend binding point @@ -970,13 +1029,19 @@ namespace MobileGL::MG_Backend::DirectGLES { if (name.empty()) continue; auto unit = currentProgram->GetUniformSamplerOrImageUnitIndex(loc); if (unit == -1) continue; + const auto uniformType = currentProgram->GetUniformType(loc); + if (IsImageUniformType(uniformType)) { + // ES image units come exclusively from the layout(binding=N) + // qualifier (preserved in the transpiled ESSL); glUniform1i on an + // image uniform is an INVALID_OPERATION. + continue; + } auto locAtBackend = g_GLESFuncs.glGetUniformLocation( backendProgramIt->second->GetBackendProgramId(), name.c_str()); g_GLESFuncs.glUniform1i(locAtBackend, unit); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); auto& samplerObject = textureUnit.GetSamplerObject(); - const auto uniformType = currentProgram->GetUniformType(loc); const auto& texture2D = textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(); const SharedPtr* rawDepthSamplerObject = &samplerObject; if (!*rawDepthSamplerObject && texture2D) { @@ -1009,25 +1074,27 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - void SetCurrentBaseInstance(Uint32 baseInstance) { + static SharedPtr GetCurrentBackendProgram() { const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram(); if (!currentProgram || !currentProgram->GetLinkStatus()) { - return; + return nullptr; } const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get()); if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) { - backendProgramIt->second->SetBaseInstance(baseInstance); + return backendProgramIt->second; + } + return nullptr; + } + + void SetCurrentBaseInstance(Uint32 baseInstance) { + if (const auto program = GetCurrentBackendProgram()) { + program->SetBaseInstance(baseInstance); } } 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); + if (const auto program = GetCurrentBackendProgram()) { + program->SetDrawID(drawId); } } @@ -1048,18 +1115,35 @@ namespace MobileGL::MG_Backend::DirectGLES { // GPU-written command fields at all - so native is never worse. Only client-memory // commands take the CPU per-command loop. static void ExecuteIndexedIndirectCommands(GLenum mode, GLenum type, SizeT indexSize, const Uint8* commandBytes, - SizeT commandOffset, Bool hasIndirectBuffer, GLsizei drawcount, - GLsizei stride, const char* label) { + SizeT commandOffset, + const SharedPtr& drawIndirectBuffer, + GLsizei drawcount, GLsizei stride, const char* label) { (void)label; - const Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws(); + const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); if (useNative) { + // gl_BaseInstance must observe GPU-written command fields; expose the indirect + // buffer to the program's mg_IndirectParams SSBO view and address it per draw. + const auto backendProgram = GetCurrentBackendProgram(); + const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; + if (paramsBinding >= 0) { + auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer); + if (resource && resource->id != 0) { + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), + resource->id); + } + } for (GLsizei i = 0; i < drawcount; ++i) { - DrawElementsIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; SetCurrentDrawID(static_cast(i)); - SetCurrentBaseInstance(cmd.baseInstance); - g_GLESFuncs.glDrawElementsIndirect( - mode, type, reinterpret_cast(commandOffset + static_cast(i) * stride)); + if (paramsBinding >= 0 && backendProgram) { + // baseInstance is the 5th word of DrawElementsIndirectCommand. + backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 16) / 4)); + } else { + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseInstance(cmd.baseInstance); + } + g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(cmdByteOffset)); } } else { for (GLsizei i = 0; i < drawcount; ++i) { @@ -1081,18 +1165,32 @@ namespace MobileGL::MG_Backend::DirectGLES { } static void ExecuteArraysIndirectCommands(GLenum mode, const Uint8* commandBytes, SizeT commandOffset, - Bool hasIndirectBuffer, GLsizei drawcount, GLsizei stride, - const char* label) { + const SharedPtr& drawIndirectBuffer, + GLsizei drawcount, GLsizei stride, const char* label) { (void)label; - const Bool useNative = hasIndirectBuffer && SupportsNativeIndirectDraws(); + const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); if (useNative) { + const auto backendProgram = GetCurrentBackendProgram(); + const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; + if (paramsBinding >= 0) { + auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer); + if (resource && resource->id != 0) { + g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), + resource->id); + } + } for (GLsizei i = 0; i < drawcount; ++i) { - DrawArraysIndirectCommand cmd{}; - std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + const SizeT cmdByteOffset = commandOffset + static_cast(i) * stride; SetCurrentDrawID(static_cast(i)); - SetCurrentBaseInstance(cmd.baseInstance); - g_GLESFuncs.glDrawArraysIndirect( - mode, reinterpret_cast(commandOffset + static_cast(i) * stride)); + if (paramsBinding >= 0 && backendProgram) { + // baseInstance is the 4th word of DrawArraysIndirectCommand. + backendProgram->SetBaseInstanceWordIndex(static_cast((cmdByteOffset + 12) / 4)); + } else { + DrawArraysIndirectCommand cmd{}; + std::memcpy(&cmd, commandBytes + static_cast(i) * stride, sizeof(cmd)); + SetCurrentBaseInstance(cmd.baseInstance); + } + g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast(cmdByteOffset)); } } else { for (GLsizei i = 0; i < drawcount; ++i) { @@ -1127,13 +1225,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - const auto& backendProgramIt = PrgramImpl::g_backendProgramObjects.find(currentProgram.get()); - if (backendProgramIt != PrgramImpl::g_backendProgramObjects.end()) { - backendProgramIt->second->Use(); - } else { - g_GLESFuncs.glUseProgram(0); - MGLOG_E("No backend program found (maybe not synced) for current compute program."); - } + // Compute shaders sample textures through the same unit bindings as draws + // (e.g. Flywheel's depth-pyramid downsample reads the depth attachment on + // unit 0), so re-establish unit bindings after the content syncs above. + BindCurrentTextures(); + // Compute programs need the same per-program resource sync as draws: + // uniform-block bindings and sampler units only exist through the API + // because layout(binding) is stripped from the transpiled ESSL. + BindCurrentProgramWithResources(); } GLuint GetBackendProgramId(GLuint program) { @@ -1267,10 +1366,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - const Bool hasIndirectBuffer = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + const auto& drawIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), - hasIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect"); + drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect"); } void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, @@ -1331,7 +1430,7 @@ namespace MobileGL::MG_Backend::DirectGLES { std::memcpy(&actualDrawCount, parameterData->data() + drawcount, sizeof(actualDrawCount)); actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); ExecuteIndexedIndirectCommands(mode, type, indexSize, drawData->data() + commandOffset, commandOffset, - /*hasIndirectBuffer=*/true, static_cast(actualDrawCount), stride, + drawBuffer, static_cast(actualDrawCount), stride, "MultiDrawElementsIndirectCount"); } @@ -1362,9 +1461,9 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - const Bool hasIndirectBuffer = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; - ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), hasIndirectBuffer, + const auto& drawIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, drawcount, stride, "MultiDrawArraysIndirect"); } @@ -1428,10 +1527,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - const Bool hasIndirectBuffer = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; + const auto& drawIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), - hasIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand), + drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand), "DrawElementsIndirect"); } @@ -1460,9 +1559,9 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - const Bool hasIndirectBuffer = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject() != nullptr; - ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), hasIndirectBuffer, 1, + const auto& drawIndirectBuffer = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, 1, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); } @@ -3526,7 +3625,27 @@ namespace MobileGL::MG_Backend::DirectGLES { } namespace { - thread_local Bool t_backendContextCurrent = false; + // The single backend ES context migrates between app threads (FCL/pojav-style + // LWJGL hands the EGL context from JVM thread to JVM thread). Ownership must + // live in ONE global slot: a per-thread flag can never be cleared on the + // LOSING thread when another thread takes (or destroys/releases) the context, + // leaving a stale "current" claim behind. A stale claim makes buffer ops issue + // GL calls that silently no-op (no context is current on that thread) while + // still updating shadow bookkeeping (bind cache, synced serials), permanently + // desynchronizing backend buffer state. + std::atomic g_backendContextOwnerThread{}; + + // Bumped whenever the backend ES context is destroyed; fence handles + // created under an older generation belong to a dead context and must + // never be passed back to GL (mirrors BufferImpl's context tracking). + Uint g_syncContextGeneration = 1; + + // Backend fence handle: a native ES sync plus the ES context + // generation it was created under. + struct GLESSyncObject { + GLsync esSync = nullptr; + Uint contextGeneration = 0; + }; } Bool MakeCurrent() { @@ -3540,7 +3659,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("DirectGLES::MakeCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } - t_backendContextCurrent = true; + g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); // The ops table may have been unregistered when a previous ES context was // destroyed (e.g. a probe context); re-register now that GL is usable. BufferImpl::RegisterBufferBackendOps(); @@ -3549,7 +3668,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool ReleaseCurrent() { if (!g_EGLFuncs.eglMakeCurrent || g_Display == EGL_NO_DISPLAY) { - t_backendContextCurrent = false; + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } if (!g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { @@ -3557,12 +3676,101 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E("DirectGLES::ReleaseCurrent failed: native eglMakeCurrent returned error 0x%04x", error); return false; } - t_backendContextCurrent = false; + // Clearing the global owner works from ANY thread (a release request can + // legally arrive on a thread other than the current owner); erring towards + // "not current" only defers buffer ops, which is always safe. + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } Bool IsBackendContextCurrentOnThisThread() { - return t_backendContextCurrent && g_Context != EGL_NO_CONTEXT; + if (g_Context == EGL_NO_CONTEXT) { + return false; + } + if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) { + return false; + } + // Belt and braces: EGL itself is the ground truth. A migration that bypassed + // MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim + // standing, or GL calls would silently no-op while shadow bookkeeping (bind + // cache, synced serials) still advances. + if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) { + return false; + } + return true; + } + + BackendSyncHandle FenceSync() { + // ES fences can only be created on the thread that owns the ES context + // (Flywheel and friends fence on the render thread, which does). + // Returning null makes the frontend fall back to an always-signaled + // sync object. + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) { + return nullptr; + } + GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + if (esSync == nullptr) { + return nullptr; + } + return new GLESSyncObject{esSync, g_syncContextGeneration}; + } + + GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + const auto* sync = static_cast(handle); + if (sync == nullptr) { + return GL_ALREADY_SIGNALED; + } + // The creating ES context is gone: its GPU work either completed or + // died with the context; waiting is meaningless either way. + if (sync->contextGeneration != g_syncContextGeneration) { + return GL_ALREADY_SIGNALED; + } + // Degraded path: a thread that does not own the ES context cannot + // issue GL calls, so report signaled instead of blocking on state we + // cannot observe. Fence waits normally arrive on the render thread, + // which owns the context. + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) { + return GL_ALREADY_SIGNALED; + } + return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout); + } + + void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + (void)flags; + (void)timeout; + const auto* sync = static_cast(handle); + if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) { + return; + } + // ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED. + g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED); + } + + void DeleteSync(BackendSyncHandle handle) { + auto* sync = static_cast(handle); + if (sync == nullptr) { + return; + } + if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() && + g_GLESFuncs.glDeleteSync) { + g_GLESFuncs.glDeleteSync(sync->esSync); + } + // Otherwise the ES sync is abandoned; the ES context reclaims all of + // its sync objects when it is destroyed. + delete sync; + } + + Bool GetSyncStatus(BackendSyncHandle handle) { + const auto* sync = static_cast(handle); + if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration || + !IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) { + return true; + } + GLint status = GL_SIGNALED; + GLsizei length = 0; + g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status); + return status == GL_SIGNALED; } void Present() { @@ -3571,7 +3779,10 @@ namespace MobileGL::MG_Backend::DirectGLES { void DestroyEGLContext() { BufferImpl::OnBackendContextDestroyed(); - t_backendContextCurrent = false; + g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); + // Outstanding fence handles now refer to a dead context; treat them as + // signaled from here on. + ++g_syncContextGeneration; if (g_Display != EGL_NO_DISPLAY) { g_EGLFuncs.eglMakeCurrent(g_Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (g_Context != EGL_NO_CONTEXT) { diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 8d4fe20d..ed645f33 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -98,6 +99,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // True when the backend ES context is current on the calling thread, i.e. // immediate buffer ops may issue GL calls right now. Bool IsBackendContextCurrentOnThisThread(); + // GL fence sync objects, backed by native ES fences. FenceSync returns null + // (the frontend then falls back to an always-signaled sync) when the calling + // thread does not own the ES context. Waits/queries degrade to "signaled" in + // the same situation, and handles created under a since-destroyed ES context + // are always treated as signaled. + BackendSyncHandle FenceSync(); + GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void DeleteSync(BackendSyncHandle sync); + Bool GetSyncStatus(BackendSyncHandle sync); void Present(); void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs); void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 97f053df..4eedbda5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -34,6 +34,9 @@ namespace MobileGL::MG_Backend::DirectGLES { 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"; + 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"; static Bool IsAngleLlvmpipeRenderer() { return g_GLESCapabilities.GLESRendererString.find("ANGLE") != String::npos && @@ -111,20 +114,28 @@ namespace MobileGL::MG_Backend::DirectGLES { if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) { return source; } - source = ReplaceIdentifier(std::move(source), "gl_BaseInstance", BASE_INSTANCE_UNIFORM_NAME); - return InjectUniformAfterVersion(std::move(source), + String replaced = ReplaceIdentifier(source, "gl_BaseInstance", BASE_INSTANCE_UNIFORM_NAME); + if (replaced == source) { + // Only a substring hit (e.g. gl_BaseInstanceARB inside a SPIRV-Cross #ifdef + // fallback); nothing was rewritten, so nothing must be declared either. + return source; + } + return InjectUniformAfterVersion(std::move(replaced), 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. + // Private globals (mg_DrawID / mg_BaseInstanceLowered / mg_BaseVertex); SPIRV-Cross then + // emits them as ordinary global declarations. mg_DrawID / mg_BaseVertex become uniforms fed + // per (sub-)draw. gl_BaseInstance is special: for indirect draws its value lives in the + // (possibly GPU-written) indirect command buffer, so its declaration expands into a + // std430 SSBO view of that buffer indexed by a CPU-computed word index, with the plain + // mg_BaseInstance uniform as the fallback for non-indirect draws. 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* name : {DRAW_ID_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 + ";"; @@ -143,6 +154,29 @@ namespace MobileGL::MG_Backend::DirectGLES { break; } } + 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); + if (pos == String::npos) { + continue; + } + const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0 + ? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1 + : 0; + String machinery; + if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) { + machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n"; + } + machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n"; + machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) + + ") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME + + " { highp uint mg_indirectWords[]; };\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 + ")"; + source.replace(pos, declaration.size(), machinery); + break; + } return source; } @@ -196,6 +230,8 @@ namespace MobileGL::MG_Backend::DirectGLES { resource.storageSize == bufferObject.GetSize(); } + + void UploadRangeNow(GLESBufferResource& resource, BufferObject& bufferObject, SizeT start, SizeT end) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -2050,6 +2086,18 @@ 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); + m_baseInstanceWordIndexUniformLocation = + g_GLESFuncs.glGetUniformLocation(m_backendProgramId, BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME); + // The mg_IndirectParams block binding is baked into the ESSL (ES cannot rebind + // SSBO blocks after compile); record it so draws bind the indirect buffer there. + m_indirectParamsBinding = -1; + if (m_baseInstanceWordIndexUniformLocation >= 0 && g_GLESFuncs.glGetProgramResourceIndex) { + const GLuint blockIndex = g_GLESFuncs.glGetProgramResourceIndex( + m_backendProgramId, GL_SHADER_STORAGE_BLOCK, INDIRECT_PARAMS_BLOCK_NAME); + if (blockIndex != GL_INVALID_INDEX && g_GLESCapabilities.MaxShaderStorageBufferBindings > 0) { + m_indirectParamsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings - 1; + } + } // Create global UBO if (stateProgramObject->GetUBOSize() > 0) { @@ -2074,10 +2122,19 @@ namespace MobileGL::MG_Backend::DirectGLES { } void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const { - if (m_baseInstanceUniformLocation < 0) { - return; + if (m_baseInstanceUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast(baseInstance)); + } + // A direct value disables the indirect-command-buffer read. + if (m_baseInstanceWordIndexUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, -1); + } + } + + void BackendProgramObjectImpl::SetBaseInstanceWordIndex(Int32 wordIndex) const { + if (m_baseInstanceWordIndexUniformLocation >= 0) { + g_GLESFuncs.glUniform1i(m_baseInstanceWordIndexUniformLocation, wordIndex); } - g_GLESFuncs.glUniform1i(m_baseInstanceUniformLocation, static_cast(baseInstance)); } void BackendProgramObjectImpl::SetDrawID(Uint32 drawId) const { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index d59611c9..204ca9f5 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include "DirectGLES.h" #include "MG_State/GLState/SamplerState/SamplerObject.h" #include "MG_State/GLState/TextureState/TextureEnum.h" @@ -132,12 +134,16 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint contextGeneration = 0; // Frontend change serial the backend storage reflects. When immediate // ops cannot run (ops unregistered, no current context), this lags and - // EnsureBufferResource falls back to a full re-upload. - Uint64 syncedChangeSerial = 0; + // EnsureBufferResource falls back to a full re-upload. Atomic: read on + // the context-owning thread while ops on other threads may update it. + std::atomic syncedChangeSerial{0}; // Ops that arrived while no ES context was current on the calling thread - // (or before storage existed); replayed by EnsureBufferResource. + // (or before storage existed); replayed by EnsureBufferResource. The ES + // context migrates between app threads, so deferring ops can race with + // the owning thread replaying them: guard both fields with pendingMutex. Bool pendingRespecify = false; VecRange1D pendingRanges; + std::mutex pendingMutex; }; // Registered as the frontend's BufferBackendOps at backend init and on @@ -317,7 +323,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncToBackend(const SharedPtr& stateProgramObject); void Use() const; void SetBaseInstance(Uint32 baseInstance) const; + void SetBaseInstanceWordIndex(Int32 wordIndex) const; void SetDrawID(Uint32 drawId) const; + Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; } Uint GetBackendProgramId() const { return m_backendProgramId; } Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; } Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; } @@ -328,6 +336,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint m_backendGlobalUBOId = 0; Int m_baseInstanceUniformLocation = -1; Int m_drawIdUniformLocation = -1; + Int m_baseInstanceWordIndexUniformLocation = -1; + Int m_indirectParamsBinding = -1; Uint32 m_snormFallbackClampOutputMask = 0; Uint32 m_unormFallbackClampOutputMask = 0; Bool m_isInitialized = false; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 7662e927..cfb13778 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -307,10 +307,35 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif + // Sampler and uniform-block bindings are re-established at draw time through the + // API, so their layout qualifiers are stripped (they may exceed ES limits). SSBO + // blocks and image uniforms are different: ES has no glShaderStorageBlockBinding, + // and image units cannot be set with glUniform1i, so for those declarations the + // binding qualifier is the only binding mechanism and must be preserved. static std::regex bindingRegex(R"(layout\s*\(\s*binding\s*=\s*\d+\s*\)\s*)"); - String result = std::regex_replace(glslCode, bindingRegex, ""); static std::regex bindingRegex2(R"(layout\s*\(\s*binding\s*=\s*\d+\s*,)"); - result = std::regex_replace(result, bindingRegex2, "layout("); + static std::regex keepBindingRegex(R"(\b(buffer|[iu]?image[A-Za-z0-9]*)\b)"); + + String result; + result.reserve(glslCode.size()); + SizeT lineStart = 0; + while (lineStart <= glslCode.size()) { + SizeT lineEnd = glslCode.find('\n', lineStart); + const Bool lastLine = lineEnd == String::npos; + String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart); + + if (!std::regex_search(line, keepBindingRegex)) { + line = std::regex_replace(line, bindingRegex, ""); + line = std::regex_replace(line, bindingRegex2, "layout("); + } + + result += line; + if (lastLine) { + break; + } + result += '\n'; + lineStart = lineEnd + 1; + } return result; } } // namespace PrgramImpl diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 86dbaab6..dc7ed7ee 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -569,6 +569,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation; funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex; funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding; + funcsTable.GL.FenceSync = FenceSync; + funcsTable.GL.ClientWaitSync = ClientWaitSync; + funcsTable.GL.WaitSync = WaitSync; + funcsTable.GL.DeleteSync = DeleteSync; + funcsTable.GL.GetSyncStatus = GetSyncStatus; funcsTableInitialized = true; } return funcsTable; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 2bf009ac..4bae0999 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1342,6 +1342,63 @@ namespace MobileGL::MG_Backend::DirectVulkan { dstY0, dstX1, dstY1, mask, filter); } + namespace { + // Backend fence handle: the VkBufferManager frame serial captured at + // fence creation. The fence is signaled once every command recorded + // under that serial has completed on the GPU (the same busy-tracking + // horizon used to recycle buffer resources). + struct VulkanSyncObject { + Uint64 frameSerial = 0; + }; + } // namespace + + BackendSyncHandle FenceSync() { + if (!pVulkanRenderer) { + return nullptr; + } + return new VulkanSyncObject{pVulkanRenderer->GetCurrentFrameSerial()}; + } + + GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + // Commands are only submitted at Present, so GL_SYNC_FLUSH_COMMANDS_BIT + // cannot force progress mid-frame; WaitForFrameSerial reports whether + // waiting can succeed at all. + (void)flags; + const auto* sync = static_cast(handle); + if (sync == nullptr || !pVulkanRenderer) { + return GL_ALREADY_SIGNALED; + } + if (pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial)) { + return GL_ALREADY_SIGNALED; + } + if (timeout == 0) { + return GL_TIMEOUT_EXPIRED; + } + return pVulkanRenderer->WaitForFrameSerial(sync->frameSerial, timeout) ? GL_CONDITION_SATISFIED + : GL_TIMEOUT_EXPIRED; + } + + void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) { + // Server-side waits are implicit: the single graphics queue executes + // submissions in order, so later GPU work already observes everything + // recorded before the fence. + (void)handle; + (void)flags; + (void)timeout; + } + + void DeleteSync(BackendSyncHandle handle) { + delete static_cast(handle); + } + + Bool GetSyncStatus(BackendSyncHandle handle) { + const auto* sync = static_cast(handle); + if (sync == nullptr || !pVulkanRenderer) { + return true; + } + return pVulkanRenderer->IsFrameSerialComplete(sync->frameSerial); + } + void Present() { MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer"); pVulkanRenderer->Present(); diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index 21635370..ac518f41 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include "Renderer/VulkanRenderer.h" namespace MobileGL::MG_Backend::DirectVulkan { @@ -89,5 +90,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTextureImage(const SharedPtr& texture, TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels); + // GL fence sync objects, mapped onto the renderer's frame-serial busy + // tracking: a fence captures the frame serial current at creation and is + // signaled once every command recorded under that serial has completed on + // the GPU. + BackendSyncHandle FenceSync(); + GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout); + void DeleteSync(BackendSyncHandle sync); + Bool GetSyncStatus(BackendSyncHandle sync); void Present(); } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 1021223a..282f6d04 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -9,6 +9,7 @@ #include "ProgramFactory.h" #include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h" +#include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/Types.h" #include @@ -311,6 +312,34 @@ namespace MobileGL::MG_Backend::DirectVulkan { return targetEnv; } + // Cheap raw-word scan for an `OpDecorate BuiltIn InstanceIndex` decoration. Used + // only to decide whether to warn when shaderDrawParameters is unavailable; a false + // negative merely suppresses a diagnostic. + Bool SpirvDeclaresInstanceIndexBuiltin(const Vector& spirv) { + constexpr Uint32 kSpirvMagicNumber = 0x07230203u; + constexpr SizeT kHeaderWordCount = 5; + if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) { + return false; + } + + SizeT wordIndex = kHeaderWordCount; + while (wordIndex < spirv.size()) { + const Uint32 firstWord = spirv[wordIndex]; + const Uint32 wordCount = firstWord >> 16; + const auto opcode = static_cast(firstWord & 0xffffu); + if (wordCount == 0 || wordIndex + wordCount > spirv.size()) { + break; + } + if (opcode == spv::Op::OpDecorate && wordCount >= 4 && + static_cast(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn && + static_cast(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) { + return true; + } + wordIndex += wordCount; + } + return false; + } + Bool IsInterfaceVariableStaticallyUsed(const Vector& spirv, Uint32 spirvId) { if (spirv.empty() || spirvId == 0) { return false; @@ -1661,6 +1690,31 @@ namespace MobileGL::MG_Backend::DirectVulkan { } else { moduleSpirvs[i] = spv; } + + // glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's + // gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage + // loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection + // below runs on the rebased words so the added BaseInstance builtin stays consistent. + if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) { + if (m_shaderDrawParametersEnabled) { + Vector rebasedSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i], + rebasedSpirv)) { + moduleSpirvs[i] = std::move(rebasedSpirv); + } else { + MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; " + "instanced draws with a non-zero baseInstance may render incorrectly", + program.GetExternalIndex()); + } + } else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) { + static Bool s_warnedInstanceIndexUnsupported = false; + if (!s_warnedInstanceIndexUnsupported) { + s_warnedInstanceIndexUnsupported = true; + MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be " + "rebased and instanced draws with a non-zero baseInstance may render incorrectly"); + } + } + } } const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 50cfec0f..12e53f92 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -166,8 +166,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } }; - explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16) - : m_device(device), m_config(config), m_maxBindings(maxBindings) { + explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, + Bool shaderDrawParametersEnabled = false) + : m_device(device), m_maxBindings(maxBindings), m_config(config), + m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) { VkProgramObject::s_device = device; } ~ProgramFactory() = default; @@ -201,6 +203,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 m_maxBindings = 0; UnorderedMap m_cache; const VulkanRendererConfig& m_config; + // True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass + // (which needs the DrawParameters capability / gl_BaseInstance builtin). + Bool m_shaderDrawParametersEnabled = false; mutable ProgramLookupCache m_lastLookup; static inline XXH64_state_t* m_hashState = XXH64_createState(); }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index a8312026..3f1678ac 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -133,11 +133,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_copyProvider = provider; } - Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const { + Uint64 VkBufferManager::GetCompletedSerial() const { const Uint64 frameCount = m_initInfo.frameCount > 0 ? m_initInfo.frameCount : 1; - Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0; - completed = std::max(completed, m_completedSerialFloor); - return resource.lastUseSerial > completed; + const Uint64 completed = m_frameSerial > frameCount ? m_frameSerial - frameCount : 0; + return std::max(completed, m_completedSerialFloor); + } + + Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const { + return resource.lastUseSerial > GetCompletedSerial(); } Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 9fac8e2a..c7a66cdd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -98,6 +98,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { void OnResourceDestroyed(SharedPtr&& resource); Uint64 GetFrameSerial() const { return m_frameSerial; } + // Highest frame serial whose GPU work is known complete; serials at or + // below it may be considered signaled. Drives IsResourceBusy and the + // backend GL fence objects. + Uint64 GetCompletedSerial() const; // Busy = potentially referenced by GPU work that has not been fenced yet // (including commands recorded for the current, unsubmitted frame). Bool IsResourceBusy(const VkBufferResource& resource) const; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 69123db2..51bc4b7d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -18,7 +18,12 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { - static constexpr VkPipelineStageFlags kGraphicsSampledReadStages = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT; + // Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule + // only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the + // depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the + // compute stage in addition to the graphics stages. + static constexpr VkPipelineStageFlags kSampledReadStages = + VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { Int maxDimension = std::max(baseTexelSize.x(), @@ -145,7 +150,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - outSrcStageMask = kGraphicsSampledReadStages; + outSrcStageMask = kSampledReadStages; outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT; return; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: @@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - outDstStageMask = kGraphicsSampledReadStages; + outDstStageMask = kSampledReadStages; outDstAccessMask = VK_ACCESS_SHADER_READ_BIT; return; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: @@ -925,7 +930,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask, - kGraphicsSampledReadStages, srcAccessMask, + kSampledReadStages, srcAccessMask, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels, resource->arrayLayers); MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex()); @@ -1532,7 +1537,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { uploadLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, - kGraphicsSampledReadStages, + kSampledReadStages, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0, outResource.mipLevels, outResource.arrayLayers); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 85f1e1b1..8530f421 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1923,7 +1923,8 @@ void main() { m_pipelineFactory = MakeUnique(m_device, m_config); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); - m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings); + m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings, + m_shaderDrawParametersFeatureEnabled); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); m_samplerManager = MakeUnique(); @@ -5416,6 +5417,41 @@ void main() { return frame.commandBuffer; } + Uint64 VulkanRenderer::GetCurrentFrameSerial() const { + return m_bufferManager.GetFrameSerial(); + } + + Bool VulkanRenderer::IsFrameSerialComplete(Uint64 serial) const { + return serial <= m_bufferManager.GetCompletedSerial(); + } + + Bool VulkanRenderer::WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs) { + (void)timeoutNs; + if (IsFrameSerialComplete(serial)) { + return true; + } + // Work recorded under the current serial has not been submitted yet + // (submission happens in Present, on this same thread), so blocking + // can never make progress; the caller reports a timeout instead. + if (serial >= m_bufferManager.GetFrameSerial()) { + return false; + } + if (m_device == VK_NULL_HANDLE || m_graphicsQueue == VK_NULL_HANDLE) { + return true; + } + // The serial was submitted but has not been observed complete. Frame + // fences are only waited on when their slot is reused, so the simplest + // safe wait is to drain the graphics queue; this over-waits (bounded + // by the in-flight frame count) but never deadlocks. + const VkResult result = vkQueueWaitIdle(m_graphicsQueue); + if (result != VK_SUCCESS) { + MGLOG_E("WaitForFrameSerial: vkQueueWaitIdle returned %d", result); + return false; + } + m_bufferManager.NotifyDeviceIdle(); + return true; + } + void VulkanRenderer::Present() { MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d468a821..61c2fa26 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -167,6 +167,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkInstance GetInstance() const; Bool IsDrawIndirectCountExtensionEnabled() const; + // GL fence support, expressed in VkBufferManager frame serials: a fence + // captures GetCurrentFrameSerial() at creation and is signaled once + // IsFrameSerialComplete() reports that serial complete (the same + // busy-tracking horizon used to recycle buffer resources). + Uint64 GetCurrentFrameSerial() const; + Bool IsFrameSerialComplete(Uint64 serial) const; + // Blocking wait for a submitted serial. Returns false when the serial + // cannot complete without further submissions (it belongs to the + // current, not-yet-presented frame) or when the wait failed. + Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs); + void RequestSwapchainResize(Uint32 width, Uint32 height); void RecreateSwapchain(); diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index bf4bc47a..78ec3116 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -239,6 +239,26 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect); } + // Flywheel-style engines write GPU-copy descriptors into a FLUSH_EXPLICIT + // persistent map and glBindBufferRange that span as an SSBO for a compute + // dispatch WITHOUT ever flushing it (undefined per spec, but real drivers' + // persistent maps alias GPU-visible memory, so it works there). MobileGL's + // persistent maps alias the CPU shadow, so those bytes would never reach the + // GPU: push every explicitly-ranged SSBO binding of such maps down right + // before each dispatch. Whole-buffer (BindBufferBase) bindings are excluded + // on purpose — ranges the app DID flush already arrived, and re-uploading a + // 16MB staging ring per dispatch would be prohibitive. + static void SyncUnflushedMappedSsboRangesForDispatch() { + const auto pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage); + for (SizeT i = 0; i < pointCount; ++i) { + auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, i); + if (!point.HasExplicitRange()) continue; + const auto& bufferObject = point.GetBoundObject(); + if (!bufferObject) continue; + bufferObject->SyncMappedRangeForGpuRead(point.GetRange()); + } + } + /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { auto dispatchCompute = MG_Backend::gBackendFunctionsTable.GL.DispatchCompute; @@ -249,6 +269,7 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + SyncUnflushedMappedSsboRangesForDispatch(); dispatchCompute(numGroupsX, numGroupsY, numGroupsZ); } @@ -262,6 +283,7 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCurrentProgramForCompute(__func__)) return; + SyncUnflushedMappedSsboRangesForDispatch(); dispatchComputeIndirect(indirect); } diff --git a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp index 635bb69d..d9b8620d 100644 --- a/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp +++ b/MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp @@ -7,56 +7,120 @@ // End of Source File Header #include "GL_Sync.h" +#include namespace MobileGL::MG_Impl::GLImpl { namespace { - int g_stubSyncObject = 0; - } + // Frontend sync object: wraps an optional backend fence handle. A null + // backend handle (backend has no fence support, or could not create a + // fence at call time) keeps the legacy always-signaled behavior. + struct SyncObject { + MG_Backend::BackendSyncHandle backendHandle = nullptr; + GLenum condition = GL_SYNC_GPU_COMMANDS_COMPLETE; + GLbitfield flags = 0; + }; - // glSync semantics not really needed right now, stubbing them out + // Sync calls may arrive from any thread (launchers migrate the context + // across JVM threads), so the live-object registry is mutex-guarded. + // Entries left at process shutdown are simply dropped; their backend + // handles die with the backend. + std::mutex g_syncObjectsMutex; + UnorderedMap g_liveSyncObjects; + + SyncObject* FindSyncObject(GLsync sync) { + const std::lock_guard lock(g_syncObjectsMutex); + const auto it = g_liveSyncObjects.find(sync); + return it != g_liveSyncObjects.end() ? it->second : nullptr; + } + } // namespace GLsync FenceSync(GLenum condition, GLbitfield flags) { - (void)condition; - (void)flags; - return reinterpret_cast(&g_stubSyncObject); + auto* syncObject = new SyncObject; + syncObject->condition = condition; + syncObject->flags = flags; + if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) { + syncObject->backendHandle = backendFenceSync(); + } + const GLsync handle = reinterpret_cast(syncObject); + const std::lock_guard lock(g_syncObjectsMutex); + g_liveSyncObjects[handle] = syncObject; + return handle; } GLboolean IsSync(GLsync sync) { - return sync != nullptr ? GL_TRUE : GL_FALSE; + return FindSyncObject(sync) != nullptr ? GL_TRUE : GL_FALSE; } GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { - (void)sync; - (void)flags; - (void)timeout; - return GL_ALREADY_SIGNALED; + const auto* syncObject = FindSyncObject(sync); + if (!syncObject) { + return GL_WAIT_FAILED; + } + const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync; + if (!backendClientWaitSync || !syncObject->backendHandle) { + return GL_ALREADY_SIGNALED; // legacy always-signaled fallback + } + return backendClientWaitSync(syncObject->backendHandle, flags, timeout); } void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { - (void)sync; - (void)flags; - (void)timeout; + const auto* syncObject = FindSyncObject(sync); + if (!syncObject) { + return; + } + const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync; + if (backendWaitSync && syncObject->backendHandle) { + backendWaitSync(syncObject->backendHandle, flags, timeout); + } } void DeleteSync(GLsync sync) { - (void)sync; + if (sync == nullptr) { + return; // glDeleteSync(0) is silently ignored + } + SyncObject* syncObject = nullptr; + { + const std::lock_guard lock(g_syncObjectsMutex); + const auto it = g_liveSyncObjects.find(sync); + if (it == g_liveSyncObjects.end()) { + return; + } + syncObject = it->second; + g_liveSyncObjects.erase(it); + } + const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; + if (backendDeleteSync && syncObject->backendHandle) { + backendDeleteSync(syncObject->backendHandle); + } + delete syncObject; } void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) { - (void)sync; + const auto* syncObject = FindSyncObject(sync); + if (!syncObject) { + if (length) { + *length = 0; + } + return; + } + GLint value = 0; switch (pname) { case GL_OBJECT_TYPE: value = GL_SYNC_FENCE; break; - case GL_SYNC_STATUS: - value = GL_SIGNALED; + case GL_SYNC_STATUS: { + const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus; + const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle || + backendGetSyncStatus(syncObject->backendHandle); + value = signaled ? GL_SIGNALED : GL_UNSIGNALED; break; + } case GL_SYNC_CONDITION: - value = GL_SYNC_GPU_COMMANDS_COMPLETE; + value = static_cast(syncObject->condition); break; case GL_SYNC_FLAGS: - value = 0; + value = static_cast(syncObject->flags); break; default: break; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index f937be91..4c9005d3 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "BufferObject.h" +#include namespace MobileGL::MG_State::GLState { namespace { @@ -150,6 +151,21 @@ namespace MobileGL::MG_State::GLState { NotifySubData(m_mappedRange.start, m_mappedRange.end - m_mappedRange.start); } + void BufferObject::SyncMappedRangeForGpuRead(Range1D range) { + if (!m_isMapped) return; + if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) return; + if (!(m_mappingAccess & BufferMappingAccessBit::Write)) return; + // Non-FLUSH_EXPLICIT persistent maps are already covered wholesale by + // SyncPersistentMappedRange. + if (!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) return; + + const SizeT start = std::max(range.start, m_mappedRange.start); + const SizeT end = std::min({range.end, m_mappedRange.end, m_size}); + if (start >= end) return; + + NotifySubData(start, end - start); + } + void BufferObject::WritebackFromBackend(DataPtr data, SizeT atOffset) { MOBILEGL_ASSERT(atOffset + data.size <= m_size, "WritebackFromBackend out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset, diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index 9327585c..09854726 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -127,6 +127,12 @@ namespace MobileGL { // Pushes the persistently-mapped write range to the backend; called by // backends at draw time (persistent maps mutate the shadow without API calls). void SyncPersistentMappedRange(); + // App-compat for FLUSH_EXPLICIT persistent maps: engines like Flywheel + // write copy descriptors into the mapping and bind that range as an SSBO + // without ever flushing it. Undefined per spec, but works on drivers whose + // persistent maps alias GPU-visible memory. Ours alias the CPU shadow, so + // callers push the GPU-read range down right before it is consumed. + void SyncMappedRangeForGpuRead(Range1D range); // Shadow-only write used when the backend copies GPU results (e.g. ReadPixels // into a pixel-pack buffer) back into the frontend mirror. Does not issue a // backend op: the backend storage already holds these bytes. diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index facc18ce..6bf96b0d 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -13,6 +13,7 @@ #include "SpirvPasses/RenameSamplerFunctionParameterPass.h" #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" #include "SpirvPasses/LowerDrawParametersPass.h" +#include "SpirvPasses/RebaseInstanceIndexPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -264,6 +265,18 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass()); + + 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 2d5a0f75..7917acda 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -27,6 +27,12 @@ namespace MobileGL { // Only for backends without native draw-parameter support (DirectGLES). static bool LowerDrawParametersForEssl(const Vector& inputBinary, Vector& outputBinary); + // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so + // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan + // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex, + // which wrongly includes baseInstance). + static bool RebaseInstanceIndexForVulkan(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 index 24270ad8..4d278524 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp @@ -30,7 +30,10 @@ namespace MobileGL { case spv::BuiltIn::BaseVertex: return "mg_BaseVertex"; case spv::BuiltIn::BaseInstance: - return "mg_BaseInstance"; + // Distinct from the mg_BaseInstance uniform: the program manager + // expands this into an expression that can read the (possibly + // GPU-written) indirect command buffer. + return "mg_BaseInstanceLowered"; case spv::BuiltIn::DrawIndex: return "mg_DrawID"; default: diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp new file mode 100644 index 00000000..2ece4125 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp @@ -0,0 +1,152 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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 "RebaseInstanceIndexPass.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; + + // Returns the Input OpVariable decorated with |builtin|, or nullptr if none. + Instruction* FindBuiltinInputVariable(IRContext* context, spv::BuiltIn builtin) { + auto* defUseMgr = context->get_def_use_mgr(); + for (auto& annotation : context->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) { + continue; + } + if (static_cast(annotation.GetSingleWordInOperand(1)) != + spv::Decoration::BuiltIn) { + continue; + } + if (static_cast(annotation.GetSingleWordInOperand(2)) != builtin) { + 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; + } + return variable; + } + return nullptr; + } + + // Creates an Input OpVariable decorated BuiltIn BaseInstance, reusing the + // pointer-to-Input-int type of the existing InstanceIndex variable, and adds it + // to every OpEntryPoint interface list. Returns the new variable's result id. + uint32_t SynthesizeBaseInstanceVariable(IRContext* context, Instruction* instanceIndexVar) { + // gl_BaseInstance has the same type as gl_InstanceIndex (Input pointer to int); + // reuse the existing pointer type instead of creating a duplicate. + const uint32_t pointerTypeId = instanceIndexVar->type_id(); + const uint32_t variableId = context->TakeNextId(); + + context->AddGlobalValue(spvtools::MakeUnique( + context, spv::Op::OpVariable, pointerTypeId, variableId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(spv::StorageClass::Input)}}})); + + context->AddAnnotationInst(spvtools::MakeUnique( + context, spv::Op::OpDecorate, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {variableId}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::BuiltIn)}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, + {static_cast(spv::BuiltIn::BaseInstance)}}})); + + // Input variables must appear in the entry point interface list. + for (Instruction& entryPoint : context->module()->entry_points()) { + entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {variableId}}); + } + + return variableId; + } + } // namespace + + spvtools::opt::Pass::Status RebaseInstanceIndexPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + + Instruction* instanceIndexVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::InstanceIndex); + if (instanceIndexVar == nullptr) { + return Status::SuccessWithoutChange; + } + const uint32_t instanceIndexVarId = instanceIndexVar->result_id(); + + // Collect every load of the InstanceIndex variable before mutating anything. + std::vector instanceLoads; + defUseMgr->ForEachUser(instanceIndexVar, [&](Instruction* user) { + if (user->opcode() == spv::Op::OpLoad && + user->GetSingleWordInOperand(0) == instanceIndexVarId) { + instanceLoads.push_back(user); + } + }); + + if (instanceLoads.empty()) { + // The builtin is declared but never read; nothing to rebase. + return Status::SuccessWithoutChange; + } + + // Referencing BaseInstance requires the DrawParameters capability. On the SPIR-V + // 1.3 target used here it is core, so no OpExtension is needed. + irContext->AddCapability(spv::Capability::DrawParameters); + + Instruction* baseInstanceVar = FindBuiltinInputVariable(irContext, spv::BuiltIn::BaseInstance); + const uint32_t baseInstanceVarId = (baseInstanceVar != nullptr) + ? baseInstanceVar->result_id() + : SynthesizeBaseInstanceVariable(irContext, instanceIndexVar); + + // Replace each `OpLoad %ty %res %instanceIndex` with + // OpLoad %ty %fresh1 %instanceIndex + // OpLoad %ty %fresh2 %baseInstance + // OpISub %ty %res %fresh1 %fresh2 + // Reusing %res on the OpISub keeps downstream use lists intact. + for (Instruction* loadInst : instanceLoads) { + const uint32_t typeId = loadInst->type_id(); + const uint32_t instanceValueId = irContext->TakeNextId(); + const uint32_t baseValueId = irContext->TakeNextId(); + + loadInst->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, typeId, instanceValueId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {instanceIndexVarId}}})); + loadInst->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, typeId, baseValueId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {baseInstanceVarId}}})); + + loadInst->SetOpcode(spv::Op::OpISub); + loadInst->SetInOperands(Instruction::OperandList{ + {SPV_OPERAND_TYPE_ID, {instanceValueId}}, + {SPV_OPERAND_TYPE_ID, {baseValueId}}}); + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken RebaseInstanceIndexPass::CreateRebaseInstanceIndexPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.h new file mode 100644 index 00000000..86f2ce05 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.h @@ -0,0 +1,37 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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 { + // glslang's relaxed-Vulkan mode aliases GL's gl_InstanceID to Vulkan's + // gl_InstanceIndex. That is wrong for OpenGL semantics: GL's gl_InstanceID is + // zero-based (excludes baseInstance) while Vulkan's InstanceIndex includes the + // draw's firstInstance. On the DirectVulkan backend any draw with baseInstance != 0 + // (Flywheel's glMultiDrawElementsIndirect, glDrawElementsInstancedBaseInstance) + // therefore feeds shaders an InstanceID offset by baseInstance. This pass rebases + // every load of the InstanceIndex builtin to (InstanceIndex - BaseInstance), the + // same lowering Zink/DXVK use, so the value seen by the shader matches GL's + // zero-based gl_InstanceID. Vulkan backend only - the DirectGLES transpile path + // already receives a zero-based gl_InstanceID from SPIRV-Cross. + class RebaseInstanceIndexPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "rebase-instance-index"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateRebaseInstanceIndexPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL