diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 1ef1318d..d8d6ef46 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2069,41 +2069,104 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - static GLuint s_prevDrawFBO = 0; - static GLuint s_prevReadFBO = 0; - void BindTempFBO(Bool isRead) { - MGLOG_D("%s: Binding temporary FBO for operations like CopyTexImage2D that require framebuffer binding, " - "previous draw FBO=%u, read FBO=%u", - __func__, s_prevDrawFBO, s_prevReadFBO); - static GLuint tempFBO = 0; - if (!tempFBO) { - g_GLESFuncs.glGenFramebuffers(1, &tempFBO); - } - if (isRead) { - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, (GLint*)&s_prevReadFBO); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, tempFBO); - } else { - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (GLint*)&s_prevDrawFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, tempFBO); - } - } - void RestoreFBOFromTemp(Bool isRead) { - if (isRead) { - MGLOG_D("%s: Restoring previous read FBO=%u", __func__, s_prevReadFBO); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_prevReadFBO); - } else { - MGLOG_D("%s: Restoring previous draw FBO=%u", __func__, s_prevDrawFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_prevDrawFBO); - } - } + // ---- Scoped driver-state guards for the readback/copy/blit emulation paths -------------------- + // These paths borrow driver state (FBO bindings, scratch-FBO attachments, PACK + // pixel-store, the pack-PBO binding, scissor) that the app never asked to + // change; every mutation is scoped by an RAII guard so no exit path can leak + // it. Saves/restores go through the DirectGLES driver-state shadows + // (Managers.h) instead of glGetIntegerv - no driver round-trips, and redundant + // rebinds/resets no-op. - class TempFBOBinder { + // Saves the driver READ/DRAW framebuffer binding(s) and restores them on exit. + // Per-instance state: nesting-safe. + class ScopedFramebufferBinding { public: - TempFBOBinder(Bool isRead) : m_isRead(isRead) { BindTempFBO(isRead); } - ~TempFBOBinder() { RestoreFBOFromTemp(m_isRead); } + ScopedFramebufferBinding(Bool saveRead, Bool saveDraw) : m_saveRead(saveRead), m_saveDraw(saveDraw) { + if (m_saveRead) m_prevRead = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Read); + if (m_saveDraw) m_prevDraw = FramebufferImpl::CurrentFramebufferBinding(FramebufferTarget::Draw); + } + ~ScopedFramebufferBinding() { + if (m_saveRead) FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, m_prevRead); + if (m_saveDraw) FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_prevDraw); + } + ScopedFramebufferBinding(const ScopedFramebufferBinding&) = delete; + ScopedFramebufferBinding& operator=(const ScopedFramebufferBinding&) = delete; private: - const Bool m_isRead = false; + const Bool m_saveRead; + const Bool m_saveDraw; + GLuint m_prevRead = 0; + GLuint m_prevDraw = 0; + }; + + // Applies a PACK pixel-store configuration and restores the previous one on exit. + class ScopedPackState { + public: + explicit ScopedPackState(const PixelStoreImpl::PackState& desired) + : m_prev(PixelStoreImpl::CurrentPackState()) { + PixelStoreImpl::ApplyPackState(desired); + } + ~ScopedPackState() { PixelStoreImpl::ApplyPackState(m_prev); } + ScopedPackState(const ScopedPackState&) = delete; + ScopedPackState& operator=(const ScopedPackState&) = delete; + + private: + const PixelStoreImpl::PackState m_prev; + }; + + // The frontend's current PACK parameters, for readbacks the ES driver serves + // directly with the client's layout. + static PixelStoreImpl::PackState PackStateFromContext() { + const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); + return {static_cast(packParams.Alignment), static_cast(packParams.RowLength), + static_cast(packParams.SkipRows), static_cast(packParams.SkipPixels)}; + } + + // Binds a pixel PACK buffer (0 = client memory) for the scope and returns the + // binding to the resting 0 state on exit, so no later readback can accidentally + // capture into a stale PBO. + class ScopedPixelPackBuffer { + public: + explicit ScopedPixelPackBuffer(GLuint id) { BufferImpl::BindPixelPackBufferId(id); } + ~ScopedPixelPackBuffer() { BufferImpl::BindPixelPackBufferId(0); } + ScopedPixelPackBuffer(const ScopedPixelPackBuffer&) = delete; + ScopedPixelPackBuffer& operator=(const ScopedPixelPackBuffer&) = delete; + }; + + // Force-disables GL_SCISSOR_TEST for the scope (emulation blits and clears are + // scissored; readback copies must not be clipped by app scissor state) and + // restores the app state on exit, tracked via the render-state shadow. + class ScopedScissorDisable { + public: + ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) { + if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST); + } + ~ScopedScissorDisable() { + if (m_wasEnabled) g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + } + ScopedScissorDisable(const ScopedScissorDisable&) = delete; + ScopedScissorDisable& operator=(const ScopedScissorDisable&) = delete; + + private: + const Bool m_wasEnabled; + }; + + // Binds the shared scratch FBO at READ (isRead) or DRAW for one temp operation, + // restoring the previous binding on exit. Attachments are managed through the + // ScratchFBOImpl attachment shadow by the caller (see Framebuffer()). + class TempFBOBinder { + public: + explicit TempFBOBinder(Bool isRead) + : m_binding(/*saveRead=*/isRead, /*saveDraw=*/!isRead), + m_target(isRead ? GL_READ_FRAMEBUFFER : GL_DRAW_FRAMEBUFFER) { + FramebufferImpl::BindFramebufferId(m_target, ScratchFBOImpl::EnsureId(Framebuffer())); + } + ScratchFBOImpl::ScratchFramebuffer& Framebuffer() const { return ScratchFBOImpl::TempFramebuffer(); } + GLenum Target() const { return m_target; } + + private: + ScopedFramebufferBinding m_binding; + const GLenum m_target; }; static Bool IsDepthOnlyFormat(TextureInternalFormat format) { @@ -2254,58 +2317,19 @@ namespace MobileGL::MG_Backend::DirectGLES { while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {} } + // Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the + // scope (GenerateMipmap must respecify texture storage while no incomplete + // user FBO is bound); restores the previous bindings on exit. class ScopedCompleteFramebufferBinding { public: - ScopedCompleteFramebufferBinding() { - GLint prevReadFBO = 0; - GLint prevDrawFBO = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevReadFBO); - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDrawFBO); - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &m_prevRenderbuffer); - m_prevReadFBO = static_cast(prevReadFBO); - m_prevDrawFBO = static_cast(prevDrawFBO); - - EnsureScratchFBO(); - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_scratchFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_scratchFBO); - } - - ~ScopedCompleteFramebufferBinding() { - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_prevReadFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_prevDrawFBO); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(m_prevRenderbuffer)); + ScopedCompleteFramebufferBinding() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, ScratchFBOImpl::EnsureCompleteTinyFramebufferId()); } private: - static void EnsureScratchFBO() { - if (s_scratchFBO != 0) { - return; - } - - g_GLESFuncs.glGenFramebuffers(1, &s_scratchFBO); - g_GLESFuncs.glGenRenderbuffers(1, &s_scratchRBO); - g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, s_scratchFBO); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, s_scratchRBO); - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); - g_GLESFuncs.glFramebufferRenderbuffer( - GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, s_scratchRBO); - const GLenum drawBuffer = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glDrawBuffers(1, &drawBuffer); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); - MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, - "GenerateMipmap scratch framebuffer is incomplete."); - } - - GLuint m_prevReadFBO = 0; - GLuint m_prevDrawFBO = 0; - GLint m_prevRenderbuffer = 0; - static GLuint s_scratchFBO; - static GLuint s_scratchRBO; + ScopedFramebufferBinding m_binding; }; - GLuint ScopedCompleteFramebufferBinding::s_scratchFBO = 0; - GLuint ScopedCompleteFramebufferBinding::s_scratchRBO = 0; - class ScopedDetachedTextureFramebufferAttachments { public: explicit ScopedDetachedTextureFramebufferAttachments( @@ -2314,13 +2338,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - GLint prevReadFBO = 0; - GLint prevDrawFBO = 0; - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevReadFBO); - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDrawFBO); - m_prevReadFBO = static_cast(prevReadFBO); - m_prevDrawFBO = static_cast(prevDrawFBO); - const auto backendTextureIt = TextureImpl::g_backendTextureObjects.find(texture.get()); if (backendTextureIt == TextureImpl::g_backendTextureObjects.end() || !backendTextureIt->second) { return; @@ -2361,7 +2378,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, backendFBOId); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId); if (attachmentObject.IsLayered()) { g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0); } else { @@ -2378,7 +2395,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ~ScopedDetachedTextureFramebufferAttachments() { for (const auto& attachment : m_detachedAttachments) { - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, attachment.framebuffer); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, attachment.framebuffer); if (attachment.layered) { g_GLESFuncs.glFramebufferTexture( GL_DRAW_FRAMEBUFFER, attachment.attachment, attachment.texture, attachment.level); @@ -2388,8 +2405,6 @@ namespace MobileGL::MG_Backend::DirectGLES { attachment.texture, attachment.level); } } - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_prevReadFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_prevDrawFBO); } private: @@ -2402,52 +2417,31 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool layered = false; }; - GLuint m_prevReadFBO = 0; - GLuint m_prevDrawFBO = 0; + // Declared first so its restore runs after the reattach loop in the dtor. + ScopedFramebufferBinding m_binding{/*saveRead=*/true, /*saveDraw=*/true}; Vector m_detachedAttachments; }; + // Binds the scratch blit READ/DRAW framebuffers with scissor forced off (blits + // are scissored) for one texture-to-texture copy; restores the bindings and the + // scissor state on exit. Attachments on the two scratch FBOs are managed by the + // blit helpers through the ScratchFBOImpl attachment shadow. class ScopedDepthBlitState { public: - ScopedDepthBlitState() { - g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, reinterpret_cast(&m_prevReadFBO)); - g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, reinterpret_cast(&m_prevDrawFBO)); - g_GLESFuncs.glGetBooleanv(GL_SCISSOR_TEST, &m_prevScissorEnabled); - g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - - if (s_readFBO == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_readFBO); - } - if (s_drawFBO == 0) { - g_GLESFuncs.glGenFramebuffers(1, &s_drawFBO); - } - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, s_readFBO); + ScopedDepthBlitState() : m_binding(/*saveRead=*/true, /*saveDraw=*/true) { + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, + ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitReadFramebuffer())); AssertNoGLError("bind depth blit read framebuffer"); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_drawFBO); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, + ScratchFBOImpl::EnsureId(ScratchFBOImpl::BlitDrawFramebuffer())); AssertNoGLError("bind depth blit draw framebuffer"); } - ~ScopedDepthBlitState() { - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_prevReadFBO); - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_prevDrawFBO); - if (m_prevScissorEnabled == GL_TRUE) { - g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - } else { - g_GLESFuncs.glDisable(GL_SCISSOR_TEST); - } - } - private: - GLuint m_prevReadFBO = 0; - GLuint m_prevDrawFBO = 0; - GLboolean m_prevScissorEnabled = GL_FALSE; - static GLuint s_readFBO; - static GLuint s_drawFBO; + ScopedScissorDisable m_scissorOff; + ScopedFramebufferBinding m_binding; }; - GLuint ScopedDepthBlitState::s_readFBO = 0; - GLuint ScopedDepthBlitState::s_drawFBO = 0; - static void BlitDepthTexture2D(GLuint srcTexture, GLint srcLevel, GLint srcX, GLint srcY, GLsizei srcWidth, GLsizei srcHeight, GLuint dstTexture, GLint dstLevel, GLint dstX, GLint dstY, GLsizei dstWidth, GLsizei dstHeight) { @@ -2458,20 +2452,17 @@ namespace MobileGL::MG_Backend::DirectGLES { ClearGLErrors(); ScopedDepthBlitState state; - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach depth blit read color texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach depth blit draw color texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, srcTexture, - srcLevel); + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureDepthAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel, + /*withStencil=*/false); AssertNoGLError("attach depth blit source texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, dstTexture, - dstLevel); + ScratchFBOImpl::EnsureDepthAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel, + /*withStencil=*/false); AssertNoGLError("attach depth blit destination texture"); - g_GLESFuncs.glReadBuffer(GL_NONE); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_NONE); AssertNoGLError("set depth blit read buffer"); - const GLenum drawBuffer = GL_NONE; - g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_NONE); AssertNoGLError("set depth blit draw buffer"); MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Depth blit read framebuffer is incomplete."); @@ -2497,24 +2488,15 @@ namespace MobileGL::MG_Backend::DirectGLES { ClearGLErrors(); ScopedDepthBlitState state; - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach color blit read depth texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach color blit draw depth texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach color blit read stencil texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - AssertNoGLError("detach color blit draw stencil texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTexture, - srcLevel); + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + auto& drawFB = ScratchFBOImpl::BlitDrawFramebuffer(); + ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); AssertNoGLError("attach color blit source texture"); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dstTexture, - dstLevel); + ScratchFBOImpl::EnsureColorAttachment2D(drawFB, GL_DRAW_FRAMEBUFFER, dstTexture, GL_TEXTURE_2D, dstLevel); AssertNoGLError("attach color blit destination texture"); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); AssertNoGLError("set color blit read buffer"); - const GLenum drawBuffer = GL_COLOR_ATTACHMENT0; - g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + ScratchFBOImpl::EnsureDrawBuffer(drawFB, GL_COLOR_ATTACHMENT0); AssertNoGLError("set color blit draw buffer"); MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Color blit read framebuffer is incomplete."); @@ -2539,75 +2521,38 @@ namespace MobileGL::MG_Backend::DirectGLES { ClearGLErrors(); ScopedDepthBlitState state; - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, srcTexture, - srcLevel); + auto& readFB = ScratchFBOImpl::BlitReadFramebuffer(); + ScratchFBOImpl::EnsureColorAttachment2D(readFB, GL_READ_FRAMEBUFFER, srcTexture, GL_TEXTURE_2D, srcLevel); AssertNoGLError("attach R32F copy source texture"); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + ScratchFBOImpl::EnsureReadBuffer(readFB, GL_COLOR_ATTACHMENT0); AssertNoGLError("set R32F copy read buffer"); MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "R32F copy read framebuffer is incomplete."); AssertNoGLError("check R32F copy read framebuffer"); - GLint prevPackBuffer = 0; - GLint prevUnpackBuffer = 0; - GLint prevPackAlignment = 4; - GLint prevUnpackAlignment = 4; - GLint prevPackRowLength = 0; - GLint prevUnpackRowLength = 0; - GLint prevPackSkipRows = 0; - GLint prevUnpackSkipRows = 0; - GLint prevPackSkipPixels = 0; - GLint prevUnpackSkipPixels = 0; - GLint prevActiveTexture = GL_TEXTURE0; - GLint prevBoundTexture = 0; - - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPackBuffer); - g_GLESFuncs.glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &prevUnpackBuffer); - g_GLESFuncs.glGetIntegerv(GL_PACK_ALIGNMENT, &prevPackAlignment); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_ALIGNMENT, &prevUnpackAlignment); - g_GLESFuncs.glGetIntegerv(GL_PACK_ROW_LENGTH, &prevPackRowLength); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_ROW_LENGTH, &prevUnpackRowLength); - g_GLESFuncs.glGetIntegerv(GL_PACK_SKIP_ROWS, &prevPackSkipRows); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_SKIP_ROWS, &prevUnpackSkipRows); - g_GLESFuncs.glGetIntegerv(GL_PACK_SKIP_PIXELS, &prevPackSkipPixels); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &prevUnpackSkipPixels); - g_GLESFuncs.glGetIntegerv(GL_ACTIVE_TEXTURE, &prevActiveTexture); - - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 4); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - Vector pixels(static_cast(width) * static_cast(height)); - g_GLESFuncs.glReadPixels(srcX, srcY, width, height, GL_RED, GL_FLOAT, pixels.data()); - AssertNoGLError("read R32F copy pixels"); + { + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); + g_GLESFuncs.glReadPixels(srcX, srcY, width, height, GL_RED, GL_FLOAT, pixels.data()); + AssertNoGLError("read R32F copy pixels"); + } - g_GLESFuncs.glActiveTexture(GL_TEXTURE0 + TextureImpl::TempTextureUnit); - g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevBoundTexture); + // Upload side: the rows are tightly packed floats, which the resting driver + // UNPACK state (4/0/0/0, maintained by ScopedDefaultUnpackState) parses + // correctly; the unpack-PBO binding rests at 0 by the same discipline (the + // call below no-ops unless something diverged). + BufferImpl::BindPixelUnpackBufferId(0); + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); g_GLESFuncs.glBindTexture(dstTarget, dstTexture); g_GLESFuncs.glTexSubImage2D(dstTarget, dstLevel, dstX, dstY, width, height, GL_RED, GL_FLOAT, pixels.data()); AssertNoGLError("upload R32F copy pixels"); - - g_GLESFuncs.glBindTexture(dstTarget, static_cast(prevBoundTexture)); - g_GLESFuncs.glActiveTexture(static_cast(prevActiveTexture)); - TextureImpl::g_activeTextureUnit = - static_cast(static_cast(prevActiveTexture) - GL_TEXTURE0); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPackBuffer)); - g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(prevUnpackBuffer)); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, prevPackAlignment); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, prevUnpackAlignment); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, prevPackRowLength); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, prevUnpackRowLength); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, prevPackSkipRows); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, prevUnpackSkipRows); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, prevPackSkipPixels); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, prevUnpackSkipPixels); + // Re-bind what the texture-binding cache says lives on the temp unit so the + // cache stays truthful without a driver query. + auto* cachedBound = + TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(MG_Util::ConvertGLEnumToTextureTarget(dstTarget))]; + g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); } static void GenerateDepthTexture2DMipmap( @@ -2720,6 +2665,10 @@ namespace MobileGL::MG_Backend::DirectGLES { }); } else { MGLOG_D("%s: Backend depth", __func__); + // nullptr means "uninitialized storage" only while no unpack PBO is + // bound; enforce the resting 0 state instead of assuming it (no-op + // through the binding cache unless something diverged). + BufferImpl::BindPixelUnpackBufferId(0); g_GLESFuncs.glTexImage2D(target, level, (GLint)internalformat, width, height, border, format, type, nullptr); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { @@ -2731,9 +2680,10 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); - GLenum attachment = isStencilFormat ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; TempFBOBinder tempFBOBinder(false); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, attachment, target, currentTex, level); + ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit + ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, + static_cast(currentTex), target, level, isStencilFormat); if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { MGLOG_E("ES glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE"); @@ -2810,9 +2760,10 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); - GLenum attachment = isStencilFormat ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; TempFBOBinder tempFBOBinder(false); - g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, attachment, target, currentTex, level); + ScopedScissorDisable scissorOff; // the depth-copy blit below is scissored like any blit + ScratchFBOImpl::EnsureDepthAttachment2D(tempFBOBinder.Framebuffer(), GL_DRAW_FRAMEBUFFER, currentTex, + target, level, isStencilFormat); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); @@ -2857,13 +2808,15 @@ namespace MobileGL::MG_Backend::DirectGLES { const GLenum backendTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); backendTexture->Bind(backendTarget, unitIndex); - DebugImpl::ErrorLopper::Clear(); // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps. // Also detach the source texture from synced FBO objects for ANGLE's validation. ScopedDetachedTextureFramebufferAttachments detachedAttachments(texture); - DebugImpl::ErrorLopper::Clear(); // Bind a complete internal FBO that does not reference the source texture. ScopedCompleteFramebufferBinding completeFramebuffer; + // ErrorLopper is compiled out at the default log level; RecordGLError below + // forwards the next queued error to the APP, so stale flags from earlier + // best-effort calls must be drained by the always-live helper. + ClearGLErrors(); g_GLESFuncs.glGenerateMipmap(backendTarget); RecordGLError("glGenerateMipmap", backendTarget, texture->GetFormat()); } @@ -2925,7 +2878,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (srcTexture->GetFormat() == TextureInternalFormat::R32F || dstTexture->GetFormat() == TextureInternalFormat::R32F) { - DebugImpl::ErrorLopper::Clear(); + // The single glGetError below decides the fallback dispatch, and + // ErrorLopper::Clear is compiled out at the default log level - drain + // with the always-live helper so a stale flag cannot misroute a + // succeeded native copy into the 2D-only fallback. + ClearGLErrors(); g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ, dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); @@ -2944,7 +2901,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - DebugImpl::ErrorLopper::Clear(); + ClearGLErrors(); g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ, dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); @@ -3167,6 +3124,11 @@ namespace MobileGL::MG_Backend::DirectGLES { FramebufferImpl::SyncCurrentFBO(); RenderStateImpl::SyncRenderState(); + // SyncCurrentFBO early-outs for the default framebuffer, so without this + // bind a user-FBO -> default-FBO switch would leave the clear landing on + // the stale driver DRAW binding (the fi/fv/uiv siblings all bind too). + BindCurrentFBO(FramebufferTarget::Draw); + g_GLESFuncs.glClearBufferiv(buffer, drawbuffer, value); } @@ -3214,74 +3176,6 @@ namespace MobileGL::MG_Backend::DirectGLES { ForceBindCurrentFBO(FramebufferTarget::Draw); } - class TempPixelStoreParameterSync { - public: - TempPixelStoreParameterSync(Bool isUnpack) : m_isUnpack(isUnpack) { - const auto& currentParams = MG_State::pGLContext->GetPixelStoreParameters(isUnpack); - m_prevParams = QueryCurrentGLPixelStoreParams(isUnpack); - Sync(isUnpack, currentParams); - } - - ~TempPixelStoreParameterSync() { Sync(m_isUnpack, m_prevParams); } - - private: - const Bool m_isUnpack; - - PixelStoreParameters m_prevParams; - - static PixelStoreParameters QueryCurrentGLPixelStoreParams(Bool isUnpack) { - PixelStoreParameters p; - if (!isUnpack) { - g_GLESFuncs.glGetIntegerv(GL_PACK_ALIGNMENT, (GLint*)&p.Alignment); - g_GLESFuncs.glGetIntegerv(GL_PACK_ROW_LENGTH, (GLint*)&p.RowLength); - g_GLESFuncs.glGetIntegerv(GL_PACK_SKIP_ROWS, (GLint*)&p.SkipRows); - g_GLESFuncs.glGetIntegerv(GL_PACK_SKIP_PIXELS, (GLint*)&p.SkipPixels); - // g_GLESFuncs.glGetIntegerv(GL_PACK_IMAGE_HEIGHT, (GLint*)&p.ImageHeight); - // g_GLESFuncs.glGetIntegerv(GL_PACK_SKIP_IMAGES, (GLint*)&p.SkipImages); - // GLint tmp; - // g_GLESFuncs.glGetIntegerv(GL_PACK_SWAP_BYTES, &tmp); - // p.SwapBytes = tmp ? true : false; - // g_GLESFuncs.glGetIntegerv(GL_PACK_LSB_FIRST, &tmp); - // p.LSBFirst = tmp ? true : false; - } else { - g_GLESFuncs.glGetIntegerv(GL_UNPACK_ALIGNMENT, (GLint*)&p.Alignment); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_ROW_LENGTH, (GLint*)&p.RowLength); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_SKIP_ROWS, (GLint*)&p.SkipRows); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, (GLint*)&p.SkipPixels); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, (GLint*)&p.ImageHeight); - g_GLESFuncs.glGetIntegerv(GL_UNPACK_SKIP_IMAGES, (GLint*)&p.SkipImages); - // GLint tmp; - // g_GLESFuncs.glGetIntegerv(GL_UNPACK_SWAP_BYTES, &tmp); - // p.SwapBytes = tmp ? true : false; - // g_GLESFuncs.glGetIntegerv(GL_UNPACK_LSB_FIRST, &tmp); - // p.LSBFirst = tmp ? true : false; - } - return p; - } - - static void Sync(Bool isUnpack, const PixelStoreParameters& params) { - if (!isUnpack) { - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, params.Alignment); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, params.RowLength); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, params.SkipRows); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, params.SkipPixels); - // g_GLESFuncs.glPixelStorei(GL_PACK_IMAGE_HEIGHT, params.ImageHeight); - // g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_IMAGES, params.SkipImages); - // g_GLESFuncs.glPixelStorei(GL_PACK_SWAP_BYTES, params.SwapBytes ? GL_TRUE : GL_FALSE); - // g_GLESFuncs.glPixelStorei(GL_PACK_LSB_FIRST, params.LSBFirst ? GL_TRUE : GL_FALSE); - } else { - g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, params.Alignment); - g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, params.RowLength); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, params.SkipRows); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, params.SkipPixels); - g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, params.ImageHeight); - g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, params.SkipImages); - // g_GLESFuncs.glPixelStorei(GL_UNPACK_SWAP_BYTES, params.SwapBytes ? GL_TRUE : GL_FALSE); - // g_GLESFuncs.glPixelStorei(GL_UNPACK_LSB_FIRST, params.LSBFirst ? GL_TRUE : GL_FALSE); - } - } - }; - static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); @@ -3293,16 +3187,17 @@ namespace MobileGL::MG_Backend::DirectGLES { } Vector raw(static_cast(width) * static_cast(height)); - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, raw.data()); - const GLenum readError = g_GLESFuncs.glGetError(); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); + GLenum readError = GL_NO_ERROR; + { + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + // Drain first: a stale flag some earlier best-effort call left queued + // must not be misattributed to this read (it would silently drop the + // whole readback in production builds where ErrorLopper is compiled out). + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, raw.data()); + readError = g_GLESFuncs.glGetError(); + } if (readError != GL_NO_ERROR) { MGLOG_E("ReadPixels: depth GL_FLOAT fallback read failed: %s", MG_Util::ConvertGLEnumToString(readError).c_str()); @@ -3350,16 +3245,15 @@ namespace MobileGL::MG_Backend::DirectGLES { } Vector raw(static_cast(width) * static_cast(height)); - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, raw.data()); - const GLenum readError = g_GLESFuncs.glGetError(); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); + GLenum readError = GL_NO_ERROR; + { + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + // Drain first: see ReadPixelsDepthFloatViaUnsignedInt. + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, raw.data()); + readError = g_GLESFuncs.glGetError(); + } if (readError != GL_NO_ERROR) { MGLOG_E("ReadPixels: stencil GL_UNSIGNED_INT fallback read failed: %s", MG_Util::ConvertGLEnumToString(readError).c_str()); @@ -3540,13 +3434,17 @@ namespace MobileGL::MG_Backend::DirectGLES { } // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the - // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds `height` rows of - // `width` texels, 4 components x GetReadbackComponentSize(wideType) bytes each. - // honorPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to GetTexImage of 3D - // images only; ReadPixels ignores them (GL 3.3 section 4.3.1). - static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei height, - const ReadbackChannelMapping& mapping, GLenum type, void* pixels, - Bool honorPackImageParams) { + // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds + // `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked), + // 4 components x GetReadbackComponentSize(wideType) bytes each. + // applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage + // of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4). + // Per the GL addressing rules, slice k row j lands at + // SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes + // + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride. + static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, + void* pixels, Bool applyPackImageParams) { const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); if (dstPixelBytes == 0) { return false; @@ -3559,23 +3457,27 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); // Destination layout is computed from the client-side PACK parameters; only the actual pixel - // rows are written so skip regions of the destination stay untouched. GL_PACK_SKIP_IMAGES - // skips whole 2D images of GL_PACK_IMAGE_HEIGHT (or `height`) rows for 3D readbacks. + // rows are written so skip regions of the destination stay untouched. const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT imageRows = static_cast(packParams.ImageHeight > 0 ? packParams.ImageHeight : height); + const SizeT imageRows = + applyPackImageParams && packParams.ImageHeight > 0 + ? static_cast(packParams.ImageHeight) + : static_cast(sliceHeight); + const SizeT dstImageStride = imageRows * dstRowStride; const SizeT skipImages = - honorPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; - const SizeT dstSkipOffset = skipImages * imageRows * dstRowStride + + applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + const SizeT dstSkipOffset = skipImages * dstImageStride + static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; const SizeT pboBaseOffset = reinterpret_cast(pixels); // with a PBO, `pixels` is an offset if (pixelPackBufferObject) { - const SizeT requiredSize = - pboBaseOffset + dstSkipOffset + static_cast(height - 1) * dstRowStride + dstRowBytes; + const SizeT requiredSize = pboBaseOffset + dstSkipOffset + + static_cast(sliceCount - 1) * dstImageStride + + static_cast(sliceHeight - 1) * dstRowStride + dstRowBytes; if (requiredSize > pixelPackBufferObject->GetSize()) { MGLOG_E("Readback conversion: pixel pack buffer is too small"); return true; @@ -3586,26 +3488,31 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT srcPixelBytes = 4 * srcComponentSize; Vector convertedRow(dstRowBytes); - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = wide + static_cast(row) * static_cast(width) * srcPixelBytes; - ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, - mapping, type); + for (GLsizei slice = 0; slice < sliceCount; ++slice) { + for (GLsizei row = 0; row < sliceHeight; ++row) { + const SizeT flatRow = static_cast(slice) * static_cast(sliceHeight) + + static_cast(row); + const Uint8* srcRow = wide + flatRow * static_cast(width) * srcPixelBytes; + ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, + mapping, type); - if (packParams.SwapBytes) { - const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; - if (groupSize > 1) { - for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { - std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); + if (packParams.SwapBytes) { + const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; + if (groupSize > 1) { + for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { + std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); + } } } - } - const SizeT dstOffset = dstSkipOffset + static_cast(row) * dstRowStride; - if (pixelPackBufferObject) { - pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes}, - pboBaseOffset + dstOffset); - } else { - Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); + const SizeT dstOffset = dstSkipOffset + static_cast(slice) * dstImageStride + + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes}, + pboBaseOffset + dstOffset); + } else { + Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); + } } } return true; @@ -3684,13 +3591,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - GLint prevPixelPackBuffer = 0; - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, 1); - g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, 0); - g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); Vector wide; GLenum wideType = GL_NONE; @@ -3719,7 +3621,6 @@ namespace MobileGL::MG_Backend::DirectGLES { break; } } - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(prevPixelPackBuffer)); if (wideType == GL_NONE) { MGLOG_E("Readback conversion: ES accepted no wide read type for format %s type %s, skipping readback", MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); @@ -3748,7 +3649,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); } - if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, + if (!StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels, honorPackImageParams)) { return false; } @@ -3765,7 +3666,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // authoritative, which holds for non-renderable formats (they can never be GPU-written). static Bool GetTexImageViaShadowConversion(MG_State::GLState::TextureObjectMipmap* textureMipmapObject, TextureUploadTarget uploadTarget, GLint level, GLsizei width, - GLsizei height, GLenum format, GLenum type, void* pixels) { + GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type, + void* pixels, Bool applyPackImageParams) { ReadbackChannelMapping mapping{}; if (!GetReadbackChannelMapping(format, mapping)) { return false; @@ -3773,7 +3675,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (GetReadbackDstPixelSize(mapping, type) == 0) { return false; } - if (width <= 0 || height <= 0) { + if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) { return true; } const auto& pixelPackBufferObject = @@ -3791,7 +3693,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool isInteger = false; Bool isSigned = false; if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA( - textureMipmapObject->GetFormat(), shadow, static_cast(width) * static_cast(height), + textureMipmapObject->GetFormat(), shadow, + static_cast(width) * static_cast(sliceHeight) * static_cast(sliceCount), wide, isInteger, isSigned)) { return false; } @@ -3800,8 +3703,8 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; - if (!StoreWideRowsToClient(wide.data(), wideType, width, height, mapping, type, pixels, - /*honorPackImageParams=*/true)) { + if (!StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels, + applyPackImageParams)) { return false; } MGLOG_D("GetTexImage: converted %s/%s from the CPU shadow copy", @@ -3845,8 +3748,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ReadPixels: BindCurrentFBO(Read)"); BindCurrentFBO(FramebufferTarget::Read); - MGLOG_D("ReadPixels: Applying TempPixelStoreParameterSync (PACK)"); - TempPixelStoreParameterSync tempPackParamsSync(false); + MGLOG_D("ReadPixels: Applying the PACK pixel-store scope"); + ScopedPackState packParamsScope(PackStateFromContext()); GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); MGLOG_D("ReadPixels: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); @@ -3884,27 +3787,28 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - // Handle PBO + // Handle PBO. The pack binding is scoped: it returns to the resting 0 state + // on every exit path, so a later readback can never land in a stale PBO + // (the driver-level binding used to stay on the user PBO after this call, + // capturing subsequent client-memory readbacks into it). auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - Bool usePBO; - GLuint prevPixelPackBuffer = 0; + Bool usePBO = false; + GLuint packBufferId = 0; if (pixelPackBufferObject) { auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject); MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex()); - usePBO = true; - if (!backendResource || backendResource->id == 0) { MGLOG_E("ReadPixels: No backend buffer found for PBO %u.", pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0); return; } - BufferImpl::BindBufferId(GL_PIXEL_PACK_BUFFER, backendResource->id); - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, (GLint*)&prevPixelPackBuffer); + usePBO = true; + packBufferId = backendResource->id; } else { - usePBO = false; MGLOG_D("ReadPixels: Not using PBO"); } + ScopedPixelPackBuffer packBufferBinding(packBufferId); MGLOG_D("ReadPixels: glReadPixels()"); DrainESErrors(); @@ -3945,8 +3849,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } else { MGLOG_E("ReadPixels: glMapBufferRange returned nullptr"); } - MGLOG_D("ReadPixels: Restoring previous pixel pack buffer binding %u", prevPixelPackBuffer); - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, prevPixelPackBuffer); } MGLOG_D("ReadPixels: finished"); } @@ -4025,24 +3927,22 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("GetTexImage: Binding temporary FBO"); TempFBOBinder tempFBOBinder(true); + auto& tempFB = tempFBOBinder.Framebuffer(); - MGLOG_D("GetTexImage: glFramebufferTexture2D(level=%d)", level); - // The temp FBO is reused across GetTexImage calls: detach the previous color attachment first - // so a failed attach below leaves the FBO incomplete instead of silently reading stale contents. - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + MGLOG_D("GetTexImage: attaching level %d to the scratch FBO", level); const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( MG_Util::ConvertGLEnumToTextureUploadTarget(target)); if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) { // ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads // of deeper slices are served from the CPU shadow instead (see the shadow-first branch). - g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, backendTexId, level, 0); + ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0); } else { - g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, - backendTexId, level); + ScratchFBOImpl::EnsureColorAttachment2D( + tempFB, GL_READ_FRAMEBUFFER, backendTexId, + backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level); } MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)"); - g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + ScratchFBOImpl::EnsureReadBuffer(tempFB, GL_COLOR_ATTACHMENT0); GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str()); @@ -4055,8 +3955,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); - MGLOG_D("GetTexImage: Applying TempPixelStoreParameterSync (PACK)"); - TempPixelStoreParameterSync tempPackParamsSync(false); + MGLOG_D("GetTexImage: Applying the PACK pixel-store scope"); + ScopedPackState packParamsScope(PackStateFromContext()); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -4092,25 +3992,29 @@ namespace MobileGL::MG_Backend::DirectGLES { // for normalized attachments), while the conversion path reads a wide format that is always // accepted and repacks on the CPU. if (convertible) { + // GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image + // readbacks; 2D targets must ignore them (GL 3.3 section 6.1.4). + const Bool applyPackImageParams = + backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY; // 3D/array images read back every slice, but the FBO path can only read one layer: - // multi-slice reads are served from the CPU shadow (depth as extra rows, tight layout). - const GLsizei shadowRows = size.y() * std::max(size.z(), 1); + // multi-slice reads are served from the CPU shadow (slice-major, tight layout). + const GLsizei sliceCount = std::max(size.z(), 1); const Bool multiSlice = size.z() > 1; if (multiSlice && GetTexImageViaShadowConversion(textureMipmapObject, MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), - shadowRows, format, type, pixels)) { + size.y(), sliceCount, format, type, pixels, applyPackImageParams)) { MGLOG_D("GetTexImage: finished via shadow conversion"); return; } if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels, - /*honorPackImageParams=*/true)) { + applyPackImageParams)) { MGLOG_D("GetTexImage: finished via client-format conversion"); return; } if (GetTexImageViaShadowConversion(textureMipmapObject, MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), - shadowRows, format, type, pixels)) { + size.y(), sliceCount, format, type, pixels, applyPackImageParams)) { MGLOG_D("GetTexImage: finished via shadow conversion"); return; } @@ -4127,26 +4031,26 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } - // Handle PBO + // Handle PBO. The pack binding is scoped: it returns to the resting 0 state + // on every exit path, so a later readback can never land in a stale PBO. auto& pixelPackBufferObject = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - Bool usePBO; - GLuint prevPixelPackBuffer = 0; + Bool usePBO = false; + GLuint packBufferId = 0; if (pixelPackBufferObject) { auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject); MGLOG_D("GetTexImage: Using PBO %u", pixelPackBufferObject->GetExternalIndex()); - usePBO = true; if (!backendResource || backendResource->id == 0) { MGLOG_E("GetTexImage: No backend buffer found for PBO %u.", pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0); return; } - BufferImpl::BindBufferId(GL_PIXEL_PACK_BUFFER, backendResource->id); - g_GLESFuncs.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, (GLint*)&prevPixelPackBuffer); + usePBO = true; + packBufferId = backendResource->id; } else { - usePBO = false; MGLOG_D("GetTexImage: Not using PBO"); } + ScopedPixelPackBuffer packBufferBinding(packBufferId); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -4173,9 +4077,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } else { MGLOG_E("ReadPixels: glMapBufferRange returned nullptr"); } - MGLOG_D("ReadPixels: Restoring previous pixel pack buffer binding %u", prevPixelPackBuffer); - - g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, prevPixelPackBuffer); } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { @@ -4531,6 +4432,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // after a MakeCurrent is cheaper than trusting a possibly-reset context. PrgramImpl::g_lastUsedBackendProgramId = 0; BufferImpl::InvalidateIndexedBufferBindingCache(); + BufferImpl::InvalidatePixelBufferBindingCaches(); + FramebufferImpl::InvalidateFramebufferBindingCache(); + PixelStoreImpl::InvalidatePackStateCache(); // eglSwapInterval requires a current context; a request made while none was // current (and dropped by the driver) is retried here. ApplyRequestedSwapInterval(); @@ -4839,6 +4743,12 @@ namespace MobileGL::MG_Backend::DirectGLES { void DestroyEGLContext() { BufferImpl::OnBackendContextDestroyed(); + ScratchFBOImpl::OnBackendContextDestroyed(); + FramebufferImpl::InvalidateFramebufferBindingCache(); + PixelStoreImpl::InvalidatePackStateCache(); + // Texture ids belong to the dying context; wrappers destroyed later must + // not glDeleteTextures a recycled name in a successor context. + ++TextureImpl::g_textureContextGeneration; 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. diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 7e08a69a..a372dd13 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -267,16 +267,26 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint g_boundArrayBufferId = 0; Bool g_boundArrayBufferKnown = false; + // Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see + // Managers.h). Resting state between operations is 0; scopes in the + // readback/upload paths bind what they need through the cache and + // return to 0, so a stale user PBO can never capture a later + // readback that meant to target client memory. + Uint g_boundPixelPackBufferId = 0; + Bool g_boundPixelPackBufferKnown = false; + Uint g_boundPixelUnpackBufferId = 0; + Bool g_boundPixelUnpackBufferKnown = false; + // Bumped whenever the backend ES context is destroyed; resources with // an older generation hold ids from a dead context. Uint g_bufferContextGeneration = 1; // Defined next to the indexed-binding shadow below; forward-declared so // every glDeleteBuffers site in this namespace can scrub stale shadow - // entries (GL resets a deleted buffer's indexed bindings to 0, and a - // recycled name matching a stale shadow entry would otherwise - // false-skip the rebind). - void ScrubIndexedBufferBindingShadowForId(Uint id); + // entries (GL resets a deleted buffer's bindings - indexed and pixel + // pack/unpack alike - to 0, and a recycled name matching a stale shadow + // entry would otherwise false-skip the rebind). + void ScrubBufferBindingShadowsForId(Uint id); // Resources whose owning BufferObject died; ids deleted at the next // sync point with a current ES context. @@ -318,10 +328,16 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) { InvalidateArrayBufferBindingCache(); } + // Pooling keeps the id alive (and thus any driver binding of it); + // drop to unknown rather than claiming the post-delete 0 state. + if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) || + (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) { + InvalidatePixelBufferBindingCaches(); + } const std::lock_guard lock(g_poolMutex); auto& bucket = g_bufferPool[r.storageSize]; if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) { - ScrubIndexedBufferBindingShadowForId(r.id); + ScrubBufferBindingShadowsForId(r.id); g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool r.id = 0; return; @@ -502,7 +518,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // Need a fresh id: glBufferStorage fails on a buffer that already has // immutable storage, and any prior mutable store is replaced anyway. if (resource->id != 0) { - ScrubIndexedBufferBindingShadowForId(resource->id); + ScrubBufferBindingShadowsForId(resource->id); g_GLESFuncs.glDeleteBuffers(1, &resource->id); resource->id = 0; } @@ -630,7 +646,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); } - ScrubIndexedBufferBindingShadowForId(glesResource->id); + ScrubBufferBindingShadowsForId(glesResource->id); g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); glesResource->id = 0; } @@ -668,6 +684,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void OnBackendContextDestroyed() { UnregisterBufferBackendOps(); ++g_bufferContextGeneration; + InvalidateArrayBufferBindingCache(); + InvalidateIndexedBufferBindingCache(); + InvalidatePixelBufferBindingCaches(); // The global-UBO ring's id and persistent map died with the context; // drop the handles (no GL) and let the next draw recreate the ring. ResetUboRingForNewContext(); @@ -694,7 +713,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) { InvalidateArrayBufferBindingCache(); } - ScrubIndexedBufferBindingShadowForId(glesResource->id); + ScrubBufferBindingShadowsForId(glesResource->id); g_GLESFuncs.glDeleteBuffers(1, &glesResource->id); glesResource->id = 0; } @@ -819,6 +838,31 @@ namespace MobileGL::MG_Backend::DirectGLES { g_boundArrayBufferKnown = false; } + void BindPixelPackBufferId(Uint id) { + if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { + return; + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id); + g_boundPixelPackBufferId = id; + g_boundPixelPackBufferKnown = true; + } + + void BindPixelUnpackBufferId(Uint id) { + if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { + return; + } + g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id); + g_boundPixelUnpackBufferId = id; + g_boundPixelUnpackBufferKnown = true; + } + + void InvalidatePixelBufferBindingCaches() { + g_boundPixelPackBufferId = 0; + g_boundPixelPackBufferKnown = false; + g_boundPixelUnpackBufferId = 0; + g_boundPixelUnpackBufferKnown = false; + } + namespace { // Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range // (same index + id + range) are skipped. isBase distinguishes a whole-buffer @@ -840,12 +884,12 @@ namespace MobileGL::MG_Backend::DirectGLES { return nullptr; } - // glDeleteBuffers resets the deleted buffer's bindings (indexed ones - // included) to 0 in the current context; mirror that in the shadow, or a - // later buffer recycling the same name with a matching range would - // false-skip its rebind. Default IndexedBufferBinding{} == base(0) == - // the post-delete GL state. - void ScrubIndexedBufferBindingShadowForId(Uint id) { + // glDeleteBuffers resets the deleted buffer's bindings (indexed and + // pixel pack/unpack ones included) to 0 in the current context; mirror + // that in the shadows, or a later buffer recycling the same name with a + // matching shadow entry would false-skip its rebind. Default + // IndexedBufferBinding{} == base(0) == the post-delete GL state. + void ScrubBufferBindingShadowsForId(Uint id) { if (id == 0) return; for (auto& binding : g_indexedUBOBindings) { if (binding.id == id) binding = {}; @@ -853,6 +897,12 @@ namespace MobileGL::MG_Backend::DirectGLES { for (auto& binding : g_indexedSSBOBindings) { if (binding.id == id) binding = {}; } + if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) { + g_boundPixelPackBufferId = 0; + } + if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) { + g_boundPixelUnpackBufferId = 0; + } } } // namespace @@ -897,7 +947,7 @@ namespace MobileGL::MG_Backend::DirectGLES { auto& bucket = g_bufferPool[oldestKey]; PooledBuffer& e = bucket[oldestIdx]; if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) { - ScrubIndexedBufferBindingShadowForId(e.id); + ScrubBufferBindingShadowsForId(e.id); g_GLESFuncs.glDeleteBuffers(1, &e.id); } g_pooledBytes -= e.size; @@ -1064,7 +1114,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration; if (!staleContext && entry.retireSerial > completed) continue; if (!staleContext && entry.id != 0) { - ScrubIndexedBufferBindingShadowForId(entry.id); + ScrubBufferBindingShadowsForId(entry.id); g_GLESFuncs.glDeleteBuffers(1, &entry.id); } g_retiredUboRings[i] = g_retiredUboRings.back(); @@ -1324,6 +1374,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif g_GLESFuncs.glGenTextures(1, &m_backendTextureId); + m_contextGeneration = g_textureContextGeneration; if (m_backendTextureId == 0) { MGLOG_E("Failed to generate texture object."); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); @@ -1332,6 +1383,27 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + BackendTextureObject::~BackendTextureObject() { + if (m_backendTextureId == 0) { + return; + } + // Scrub every driver-state shadow that could false-skip when the name + // or this heap address is recycled - regardless of whether the id can + // still be deleted. + ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); + for (auto& unitCache : g_boundTexturesCache) { + for (auto& boundTexture : unitCache) { + if (boundTexture == this) { + boundTexture = nullptr; + } + } + } + if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) { + g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); + } + m_backendTextureId = 0; + } + void BackendTextureObject::Bind(GLenum target, Uint unit) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -1364,7 +1436,10 @@ namespace MobileGL::MG_Backend::DirectGLES { void BackendTextureObject::RecreateBackendTexture() { if (m_backendTextureId != 0) { - g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); + ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); + if (m_contextGeneration == g_textureContextGeneration) { + g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId); + } for (auto& unitCache : g_boundTexturesCache) { for (auto& boundTexture : unitCache) { if (boundTexture == this) { @@ -1375,6 +1450,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } g_GLESFuncs.glGenTextures(1, &m_backendTextureId); + m_contextGeneration = g_textureContextGeneration; if (m_backendTextureId == 0) { MGLOG_E("Failed to regenerate texture object."); MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str()); @@ -1391,7 +1467,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // glGetIntegerv - that query forces a driver pipeline sync and, because texture // uploads run it per dirty texture per frame, it dominated the DirectGLES draw // path. The backend unpack state is set ONLY by MobileGL's own save/restore - // helpers (this class, TempPixelStoreParameterSync, the R32F copy path), all of + // helpers (this class and, historically, the R32F copy path), all of // which restore to the resting default, so the shadow stays accurate; a one-time // forced sync pins the backend to that known default up front. Apply() is // compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op. @@ -2365,6 +2441,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } Uint g_activeTextureUnit = 0; + Uint g_textureContextGeneration = 1; Array, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundTexturesCache; @@ -2390,9 +2467,59 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif if (target == FramebufferTarget::Read) - g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_backendFBOId); + BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId); else - g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId); + BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId); + } + + namespace { + // Driver-level framebuffer-binding shadow (see Managers.h). Indexed by + // FramebufferTarget {Draw, Read}. + Array g_driverFBOBindings = {0, 0}; + Array g_driverFBOBindingKnown = {false, false}; + } // namespace + + void BindFramebufferId(GLenum fbTarget, Uint id) { + const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; + const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER; + const SizeT drawIdx = SizeT(FramebufferTarget::Draw); + const SizeT readIdx = SizeT(FramebufferTarget::Read); + const Bool drawMatches = + !bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id); + const Bool readMatches = + !bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id); + if (drawMatches && readMatches) { + return; + } + g_GLESFuncs.glBindFramebuffer(fbTarget, id); + if (bindsDraw) { + g_driverFBOBindings[drawIdx] = id; + g_driverFBOBindingKnown[drawIdx] = true; + } + if (bindsRead) { + g_driverFBOBindings[readIdx] = id; + g_driverFBOBindingKnown[readIdx] = true; + } + } + + Uint CurrentFramebufferBinding(FramebufferTarget target) { + const SizeT idx = SizeT(target); + if (!g_driverFBOBindingKnown[idx]) { + // Cold path: pin the shadow from the driver once (init probes and + // pre-shadow code bind raw but restore what they found). + GLint binding = 0; + g_GLESFuncs.glGetIntegerv( + target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING, + &binding); + g_driverFBOBindings[idx] = static_cast(binding); + g_driverFBOBindingKnown[idx] = true; + } + return g_driverFBOBindings[idx]; + } + + void InvalidateFramebufferBindingCache() { + g_driverFBOBindings = {0, 0}; + g_driverFBOBindingKnown = {false, false}; } void BackendFramebufferObject::InvalidateSyncedState() { @@ -2446,7 +2573,13 @@ namespace MobileGL::MG_Backend::DirectGLES { if (glTextureTarget == GL_UNKNOWN_MGL) { glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget()); } - backendTextureObject->Bind(glTextureTarget); + // glBindTexture rejects cube-face enums (INVALID_ENUM with no + // bind, while Bind() would still record the cube-map cache slot + // as bound): bind via the owning cube target; the attach below + // keeps the face target. + const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && + glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; + backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget); g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget, backendTextureObject->GetBackendTextureId(), static_cast(attachmentObject.GetTextureLevel())); @@ -2746,6 +2879,295 @@ namespace MobileGL::MG_Backend::DirectGLES { g_fboSyncedObjects = {}; } // namespace FramebufferImpl + namespace ScratchFBOImpl { + namespace { + ScratchFramebuffer g_tempFramebuffer; + ScratchFramebuffer g_blitReadFramebuffer; + ScratchFramebuffer g_blitDrawFramebuffer; + Uint g_completeTinyFBOId = 0; + Uint g_completeTinyRBOId = 0; + + // Detach every point the shadow no longer vouches for. Used when the + // shadow is unknown (context reset, texture id deleted while attached). + void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + fb.colorTex = 0; + fb.colorTarget = 0; + fb.colorLevel = 0; + fb.colorLayer = -1; + fb.depthTex = 0; + fb.depthTarget = 0; + fb.depthLevel = 0; + fb.depthHasStencil = false; + fb.attachmentsKnown = true; + } + + void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) { + if (!fb.attachmentsKnown) { + ScrubAllAttachments(fb, fbTarget); + } + } + + // The post-attach glGetError probe below must not misread an error some + // earlier operation left queued; drain before attaching (rare path - + // only runs when the attachment actually changes). + void DrainPendingGLErrors() { + while (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + } + + // Record the color point as detached when the shadow said something was + // there; the actual detach call is the caller's (it may be replaced by + // the new attach directly when the point is being overwritten). + void RecordNoColor(ScratchFramebuffer& fb) { + fb.colorTex = 0; + fb.colorTarget = 0; + fb.colorLevel = 0; + fb.colorLayer = -1; + } + + void RecordNoDepth(ScratchFramebuffer& fb) { + fb.depthTex = 0; + fb.depthTarget = 0; + fb.depthLevel = 0; + fb.depthHasStencil = false; + } + } // namespace + + ScratchFramebuffer& TempFramebuffer() { + return g_tempFramebuffer; + } + ScratchFramebuffer& BlitReadFramebuffer() { + return g_blitReadFramebuffer; + } + ScratchFramebuffer& BlitDrawFramebuffer() { + return g_blitDrawFramebuffer; + } + + Uint EnsureId(ScratchFramebuffer& fb) { + if (fb.id == 0) { + g_GLESFuncs.glGenFramebuffers(1, &fb.id); + // A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw + // buffers (the ES defaults for a non-default framebuffer). + fb.attachmentsKnown = true; + RecordNoColor(fb); + RecordNoDepth(fb); + fb.readBuffer = GL_COLOR_ATTACHMENT0; + fb.drawBuffer = GL_COLOR_ATTACHMENT0; + } + return fb.id; + } + + void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, + GLint level) { + PrepareForUse(fb, fbTarget); + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + RecordNoDepth(fb); + } + if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) { + return; + } + if (fb.colorTex != 0) { + // Detach first: if the new attach fails, the point must read as + // missing (incomplete FBO), not silently keep the old texture. + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoColor(fb); + return; + } + fb.colorTex = tex; + fb.colorTarget = texTarget; + fb.colorLevel = level; + fb.colorLayer = -1; + } + + void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) { + PrepareForUse(fb, fbTarget); + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + RecordNoDepth(fb); + } + if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) { + return; + } + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoColor(fb); + return; + } + fb.colorTex = tex; + fb.colorTarget = 0; + fb.colorLevel = level; + fb.colorLayer = layer; + } + + void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level, + Bool withStencil) { + PrepareForUse(fb, fbTarget); + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + RecordNoColor(fb); + } + if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level && + fb.depthHasStencil == withStencil) { + return; + } + if (fb.depthTex != 0) { + // One call clears both depth and stencil points regardless of how + // the previous attachment was made. + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + } + DrainPendingGLErrors(); + g_GLESFuncs.glFramebufferTexture2D(fbTarget, + withStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT, + texTarget, tex, level); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + RecordNoDepth(fb); + return; + } + fb.depthTex = tex; + fb.depthTarget = texTarget; + fb.depthLevel = level; + fb.depthHasStencil = withStencil; + } + + void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget) { + PrepareForUse(fb, fbTarget); + if (fb.colorTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + RecordNoColor(fb); + } + } + + void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget) { + PrepareForUse(fb, fbTarget); + if (fb.depthTex != 0) { + g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + RecordNoDepth(fb); + } + } + + void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) { + if (fb.readBuffer == readBuffer) { + return; + } + g_GLESFuncs.glReadBuffer(readBuffer); + fb.readBuffer = readBuffer; + } + + void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) { + if (fb.drawBuffer == drawBuffer) { + return; + } + g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + fb.drawBuffer = drawBuffer; + } + + Uint EnsureCompleteTinyFramebufferId() { + if (g_completeTinyFBOId != 0) { + return g_completeTinyFBOId; + } + // One-time creation: the renderbuffer binding is context state with no + // shadow, so save/restore it by query here (cold path only). + GLint prevRenderbuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); + g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId); + g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId); + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId); + g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1); + g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + g_completeTinyRBOId); + const GLenum drawBuffer = GL_COLOR_ATTACHMENT0; + g_GLESFuncs.glDrawBuffers(1, &drawBuffer); + g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0); + MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, + "Scratch 1x1 framebuffer is incomplete."); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(prevRenderbuffer)); + return g_completeTinyFBOId; + } + + void NoteTextureIdDeleted(Uint textureId) { + if (textureId == 0) { + return; + } + for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) { + if (fb->colorTex == textureId || fb->depthTex == textureId) { + fb->attachmentsKnown = false; + } + } + } + + void OnBackendContextDestroyed() { + g_tempFramebuffer = {}; + g_blitReadFramebuffer = {}; + g_blitDrawFramebuffer = {}; + g_completeTinyFBOId = 0; + g_completeTinyRBOId = 0; + } + } // namespace ScratchFBOImpl + + namespace PixelStoreImpl { + namespace { + PackState g_packState; + Bool g_packStateKnown = false; + + void PinPackState(const PackState& value) { + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment); + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows); + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels); + g_packState = value; + g_packStateKnown = true; + } + } // namespace + + void ApplyPackState(const PackState& desired) { + if (!g_packStateKnown) { + PinPackState(desired); + return; + } + if (desired.Alignment != g_packState.Alignment) { + g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment); + g_packState.Alignment = desired.Alignment; + } + if (desired.RowLength != g_packState.RowLength) { + g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength); + g_packState.RowLength = desired.RowLength; + } + if (desired.SkipRows != g_packState.SkipRows) { + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows); + g_packState.SkipRows = desired.SkipRows; + } + if (desired.SkipPixels != g_packState.SkipPixels) { + g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels); + g_packState.SkipPixels = desired.SkipPixels; + } + } + + PackState CurrentPackState() { + if (!g_packStateKnown) { + // Fresh/unknown context: pin to the GL defaults (what a new context + // starts with; writing them makes the shadow authoritative either way). + PinPackState(PackState{}); + } + return g_packState; + } + + void InvalidatePackStateCache() { + g_packStateKnown = false; + } + } // namespace PixelStoreImpl + namespace PrgramImpl { Uint32 g_snormFallbackClampOutputMask = 0; Uint32 g_unormFallbackClampOutputMask = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index a493f73a..c05c4899 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -178,6 +178,17 @@ namespace MobileGL::MG_Backend::DirectGLES { // glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER. void BindBufferId(GLenum target, Uint id); void InvalidateArrayBufferBindingCache(); + // Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER + // bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel + // upload site routes its binding through these so the shadow always matches + // the driver; the resting state between operations is 0, which keeps any + // path that implicitly assumes "no PBO bound" correct. Scrubbed when a + // buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0, + // and a recycled name matching the shadow would false-skip the rebind) and + // invalidated on MakeCurrent (context may reset). + void BindPixelPackBufferId(Uint id); + void BindPixelUnpackBufferId(Uint id); + void InvalidatePixelBufferBindingCaches(); // Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on // GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the // (id, range) already at that index matches, like the array-buffer/texture/ @@ -332,6 +343,12 @@ namespace MobileGL::MG_Backend::DirectGLES { class BackendTextureObject { public: BackendTextureObject(); + // Deletes the GL texture (frontend glDeleteTextures used to leak every + // backend id for the context lifetime) and scrubs the binding/scratch-FBO + // shadows so a recycled name or heap address cannot false-skip a rebind. + ~BackendTextureObject(); + BackendTextureObject(const BackendTextureObject&) = delete; + BackendTextureObject& operator=(const BackendTextureObject&) = delete; void SyncMipmapsToBackend(const SharedPtr& stateTextureObject); void SyncBuiltinSamplerToBackend(const SharedPtr& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr& stateTextureObject); @@ -343,6 +360,9 @@ namespace MobileGL::MG_Backend::DirectGLES { void RecreateBackendTexture(); Uint m_backendTextureId = 0; + // ES context generation the id was created under; a dtor running after + // that context died must not delete a foreign (recycled) name. + Uint m_contextGeneration = 0; Bool m_isInitialized = false; Bool m_imageBindableStorageRequired = false; Bool m_backendStorageImmutable = false; @@ -367,6 +387,9 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundTexturesCache; extern Uint g_activeTextureUnit; + // Bumped when the backend ES context is destroyed; texture ids stamped with + // an older generation belong to a dead context and must not be deleted. + extern Uint g_textureContextGeneration; } // namespace TextureImpl namespace FramebufferImpl { @@ -418,8 +441,99 @@ namespace MobileGL::MG_Backend::DirectGLES { extern Array g_fboSyncedObjectVersions; extern Array g_fboSyncedObjects; + + // Driver-level READ/DRAW framebuffer-binding shadow. Every backend + // glBindFramebuffer routes through BindFramebufferId so scoped helpers can + // save/restore the current binding without a glGetIntegerv round-trip (that + // query forces a driver pipeline sync) and so redundant rebinds no-op. + // Starts unknown; the first CurrentFramebufferBinding() query pins it from + // the driver once. Invalidated on MakeCurrent (context may reset). + // GL_FRAMEBUFFER binds both targets. + void BindFramebufferId(GLenum fbTarget, Uint id); + Uint CurrentFramebufferBinding(FramebufferTarget target); + void InvalidateFramebufferBindingCache(); } // namespace FramebufferImpl + // Shared scratch framebuffers for the readback/copy/blit emulation paths, with a + // driver-side attachment shadow: repeated uses skip redundant detach/attach GL + // calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL + // texture) is detached exactly when a later use of another aspect would + // otherwise inherit it (stale cross-aspect attachments made the shared temp FBO + // incomplete and silently degraded later readbacks). + namespace ScratchFBOImpl { + struct ScratchFramebuffer { + Uint id = 0; + // false => attachment state unknown; scrub every point on next use. + // A fresh FBO starts with nothing attached, so creation sets it true. + Bool attachmentsKnown = false; + Uint colorTex = 0; + GLenum colorTarget = 0; + GLint colorLevel = 0; + GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer + Uint depthTex = 0; + GLenum depthTarget = 0; + GLint depthLevel = 0; + Bool depthHasStencil = false; + // Per-FBO read/draw buffer state (0 = unknown, set on first use). + GLenum readBuffer = 0; + GLenum drawBuffer = 0; + }; + ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW + ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source + ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination + // Returns the GL id, generating it if needed (requires a current ES context). + Uint EnsureId(ScratchFramebuffer& fb); + // The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers + // target the READ/DRAW binding respectively). Each Ensure* performs the + // minimal detach/attach set and keeps the shadow in sync; a failed attach + // records the point as detached so the completeness check fails instead of + // silently reading a stale attachment. + void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level); + void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer); + void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level, + Bool withStencil); + void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget); + void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget); + void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer); + void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer); + // A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete + // binding while respecifying texture storage). Attachment is set once at + // creation and never changes. + Uint EnsureCompleteTinyFramebufferId(); + // A backend texture id is being deleted or respecified: a scratch FBO still + // referencing it would hold a dangling attachment (ES only auto-detaches + // from the *bound* framebuffer), and a recycled name could false-skip a + // re-attach; force a full scrub on next use. + void NoteTextureIdDeleted(Uint textureId); + // The ES context (and the scratch FBO ids with it) is going away. + void OnBackendContextDestroyed(); + } // namespace ScratchFBOImpl + + // Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the + // upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state + // is written ONLY through ApplyPackState, so scoped helpers can save/restore it + // from the shadow instead of glGetIntegerv (which forces a driver pipeline + // sync), and redundant glPixelStorei calls no-op. The first Apply/Current call + // pins the driver to the shadow by writing all fields once. Invalidated on + // MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/ + // LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the + // frontend context state instead. + namespace PixelStoreImpl { + struct PackState { + GLint Alignment = 4; + GLint RowLength = 0; + GLint SkipRows = 0; + GLint SkipPixels = 0; + Bool operator==(const PackState& o) const { + return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows && + SkipPixels == o.SkipPixels; + } + }; + void ApplyPackState(const PackState& desired); + PackState CurrentPackState(); + void InvalidatePackStateCache(); + } // namespace PixelStoreImpl + // 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. diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 615a4724..a3ba28ba 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -1431,3 +1431,288 @@ TEST(RenderStateSanity, PrimitiveRestartIndexStoresAndReadsBack) { MG_State::pGLContext.reset(); } + + +// ---- DirectGLES readback driver-state shadows ---------------------------------------------------- +// Regression coverage for the readback-path state-leak overhaul: the pixel-PBO +// binding cache, the framebuffer-binding shadow, the PACK pixel-store shadow and +// the scratch-FBO attachment shadow must (a) leave the driver in the documented +// resting state, (b) skip redundant GL calls, and (c) scrub correctly on +// deletion. All drive the real Managers.cpp implementations against a recording +// mock GLES table. +namespace { + struct StateGuardCallLog { + MobileGL::Vector calls; + + MobileGL::SizeT Count(const MobileGL::String& prefix) const { + MobileGL::SizeT n = 0; + for (const auto& c : calls) { + if (c.compare(0, prefix.size(), prefix) == 0) ++n; + } + return n; + } + }; + + StateGuardCallLog* g_stateGuardLog = nullptr; + GLuint g_nextStateGuardFBOId = 201; + + void SG_Log(MobileGL::String entry) { + if (g_stateGuardLog) g_stateGuardLog->calls.push_back(MobileGL::Move(entry)); + } + void SG_BindBuffer(GLenum target, GLuint buffer) { + SG_Log("BindBuffer:" + std::to_string(target) + ":" + std::to_string(buffer)); + } + void SG_BindFramebuffer(GLenum target, GLuint framebuffer) { + SG_Log("BindFramebuffer:" + std::to_string(target) + ":" + std::to_string(framebuffer)); + } + void SG_GetIntegerv(GLenum pname, GLint* data) { + SG_Log("GetIntegerv:" + std::to_string(pname)); + if (data) *data = 0; + } + void SG_PixelStorei(GLenum pname, GLint param) { + SG_Log("PixelStorei:" + std::to_string(pname) + ":" + std::to_string(param)); + } + void SG_GenFramebuffers(GLsizei count, GLuint* framebuffers) { + for (GLsizei i = 0; i < count; ++i) framebuffers[i] = g_nextStateGuardFBOId++; + } + void SG_FramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) { + SG_Log("FramebufferTexture2D:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" + + std::to_string(textarget) + ":" + std::to_string(texture) + ":" + std::to_string(level)); + } + void SG_FramebufferTextureLayer(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer) { + SG_Log("FramebufferTextureLayer:" + std::to_string(target) + ":" + std::to_string(attachment) + ":" + + std::to_string(texture) + ":" + std::to_string(level) + ":" + std::to_string(layer)); + } + void SG_ReadBuffer(GLenum src) { + SG_Log("ReadBuffer:" + std::to_string(src)); + } + void SG_DrawBuffers(GLsizei n, const GLenum* bufs) { + SG_Log("DrawBuffers:" + std::to_string(n) + ":" + std::to_string(n > 0 && bufs ? bufs[0] : 0)); + } + GLenum SG_NoError() { + return GL_NO_ERROR; + } + + // Installs the recording table and resets every readback driver-state shadow on + // both ends, so these tests cannot bleed into (or inherit from) other tests. + struct ScopedStateGuardMocks { + ScopedStateGuardMocks(): previousFunctions(MobileGL::MG_Backend::DirectGLES::g_GLESFuncs) { + ResetShadows(); + MobileGL::MG_External::GLESFunctionsTable functions{}; + functions.glBindBuffer = SG_BindBuffer; + functions.glBindFramebuffer = SG_BindFramebuffer; + functions.glGetIntegerv = SG_GetIntegerv; + functions.glPixelStorei = SG_PixelStorei; + functions.glGenFramebuffers = SG_GenFramebuffers; + functions.glFramebufferTexture2D = SG_FramebufferTexture2D; + functions.glFramebufferTextureLayer = SG_FramebufferTextureLayer; + functions.glReadBuffer = SG_ReadBuffer; + functions.glDrawBuffers = SG_DrawBuffers; + functions.glGetError = SG_NoError; + MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(functions); + g_stateGuardLog = &log; + } + + ~ScopedStateGuardMocks() { + g_stateGuardLog = nullptr; + MobileGL::MG_Backend::DirectGLES::SetGLESFuncsTable(previousFunctions); + ResetShadows(); + } + + ScopedStateGuardMocks(const ScopedStateGuardMocks&) = delete; + ScopedStateGuardMocks& operator=(const ScopedStateGuardMocks&) = delete; + + static void ResetShadows() { + MobileGL::MG_Backend::DirectGLES::BufferImpl::InvalidatePixelBufferBindingCaches(); + MobileGL::MG_Backend::DirectGLES::FramebufferImpl::InvalidateFramebufferBindingCache(); + MobileGL::MG_Backend::DirectGLES::PixelStoreImpl::InvalidatePackStateCache(); + MobileGL::MG_Backend::DirectGLES::ScratchFBOImpl::OnBackendContextDestroyed(); + } + + StateGuardCallLog log; + MobileGL::MG_External::GLESFunctionsTable previousFunctions; + }; +} // namespace + +TEST(DirectGLESStateGuards, PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + BufferImpl::BindPixelPackBufferId(5); + EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u); + BufferImpl::BindPixelPackBufferId(5); // redundant: must not reach the driver + EXPECT_EQ(mocks.log.Count("BindBuffer:"), 1u); + BufferImpl::BindPixelPackBufferId(0); // scope exit: resting state + EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u); + BufferImpl::BindPixelPackBufferId(0); + EXPECT_EQ(mocks.log.Count("BindBuffer:"), 2u); + + // After invalidation (MakeCurrent / context reset) the first bind must reach + // the driver again even for the same value. + BufferImpl::InvalidatePixelBufferBindingCaches(); + BufferImpl::BindPixelPackBufferId(0); + EXPECT_EQ(mocks.log.Count("BindBuffer:"), 3u); +} + +TEST(DirectGLESStateGuards, FramebufferBindingShadowPinsOnceThenSkips) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + // Cold path: one driver query pins the shadow; further reads are free. + (void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read); + EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u); + (void)FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Read); + EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u); + + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7); + EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7); + EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 1u); + // GL_FRAMEBUFFER touches both targets; DRAW is still unknown so it must bind. + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7); + EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u); + // Both halves now match: no further calls for either single target. + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, 7); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 7); + FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, 7); + EXPECT_EQ(mocks.log.Count("BindFramebuffer:"), 2u); + EXPECT_EQ(FramebufferImpl::CurrentFramebufferBinding(MobileGL::FramebufferTarget::Draw), 7u); + EXPECT_EQ(mocks.log.Count("GetIntegerv:"), 1u); // shadow answered, no new query +} + +TEST(DirectGLESStateGuards, PackStateShadowAppliesMinimalDeltas) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + // First application pins all four parameters. + PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0}); + EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u); + // Identical state: zero driver calls. + PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{4, 0, 0, 0}); + EXPECT_EQ(mocks.log.Count("PixelStorei:"), 4u); + // One field changed: exactly one driver call. + PixelStoreImpl::ApplyPackState(PixelStoreImpl::PackState{1, 0, 0, 0}); + EXPECT_EQ(mocks.log.Count("PixelStorei:"), 5u); + + const auto current = PixelStoreImpl::CurrentPackState(); + EXPECT_EQ(current.Alignment, 1); + EXPECT_EQ(current.RowLength, 0); + EXPECT_EQ(current.SkipRows, 0); + EXPECT_EQ(current.SkipPixels, 0); +} + +TEST(DirectGLESStateGuards, ScratchFBODetachesCrossAspectResidue) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + auto& fb = ScratchFBOImpl::TempFramebuffer(); + EXPECT_NE(ScratchFBOImpl::EnsureId(fb), 0u); + + // A depth copy leaves a DEPTH_STENCIL attachment (the pre-fix code never + // detached it, wedging every later color readback through this FBO). + ScratchFBOImpl::EnsureDepthAttachment2D(fb, GL_DRAW_FRAMEBUFFER, 11, GL_TEXTURE_2D, 0, /*withStencil=*/true); + const MobileGL::String dsAttach = "FramebufferTexture2D:" + std::to_string(GL_DRAW_FRAMEBUFFER) + ":" + + std::to_string(GL_DEPTH_STENCIL_ATTACHMENT); + EXPECT_EQ(mocks.log.Count(dsAttach), 1u); + + // The next color use must detach the stale depth-stencil attachment exactly once. + mocks.log.calls.clear(); + ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0); + const MobileGL::String dsDetach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" + + std::to_string(GL_DEPTH_STENCIL_ATTACHMENT) + ":" + + std::to_string(GL_TEXTURE_2D) + ":0:0"; + const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" + + std::to_string(GL_COLOR_ATTACHMENT0) + ":" + + std::to_string(GL_TEXTURE_2D) + ":22:0"; + EXPECT_EQ(mocks.log.Count(dsDetach), 1u); + EXPECT_EQ(mocks.log.Count(colorAttach), 1u); + + // Back-to-back identical color use: no driver traffic at all. + mocks.log.calls.clear(); + ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0); + EXPECT_EQ(mocks.log.Count("FramebufferTexture2D:"), 0u); +} + +TEST(DirectGLESStateGuards, ScratchFBOTextureDeletionForcesFullScrub) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + auto& fb = ScratchFBOImpl::TempFramebuffer(); + ScratchFBOImpl::EnsureId(fb); + ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0); + + // The attached texture id dies: the shadow can no longer vouch for the FBO + // (ES does not auto-detach from unbound FBOs, and the name may be recycled), + // so the next use must scrub and re-attach instead of skipping. + ScratchFBOImpl::NoteTextureIdDeleted(22); + mocks.log.calls.clear(); + ScratchFBOImpl::EnsureColorAttachment2D(fb, GL_READ_FRAMEBUFFER, 22, GL_TEXTURE_2D, 0); + EXPECT_GE(mocks.log.Count("FramebufferTexture2D:"), 2u); // scrub (color + depth) ... + const MobileGL::String colorAttach = "FramebufferTexture2D:" + std::to_string(GL_READ_FRAMEBUFFER) + ":" + + std::to_string(GL_COLOR_ATTACHMENT0) + ":" + + std::to_string(GL_TEXTURE_2D) + ":22:0"; + EXPECT_EQ(mocks.log.Count(colorAttach), 1u); // ... then the real re-attach +} + +TEST(DirectGLESStateGuards, ScratchFBOReadDrawBufferStateCached) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedStateGuardMocks mocks; + + auto& fb = ScratchFBOImpl::BlitReadFramebuffer(); + ScratchFBOImpl::EnsureId(fb); + + // Fresh FBOs default to COLOR_ATTACHMENT0 for both buffers: no call needed. + ScratchFBOImpl::EnsureReadBuffer(fb, GL_COLOR_ATTACHMENT0); + EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 0u); + // Depth blits want GL_NONE; the transition costs one call, repeats are free. + ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE); + ScratchFBOImpl::EnsureReadBuffer(fb, GL_NONE); + EXPECT_EQ(mocks.log.Count("ReadBuffer:"), 1u); + ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE); + ScratchFBOImpl::EnsureDrawBuffer(fb, GL_NONE); + EXPECT_EQ(mocks.log.Count("DrawBuffers:"), 1u); +} + +namespace { + MobileGL::Vector* g_deletedTextureIds = nullptr; + + void SG_DeleteTextures(GLsizei count, const GLuint* textures) { + if (!g_deletedTextureIds) return; + for (GLsizei i = 0; i < count; ++i) g_deletedTextureIds->push_back(textures[i]); + } +} // namespace + +TEST(DirectGLESBackendTexture, DestructorDeletesIdAndScrubsBindingCache) { + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedDirectGLESTextureBindings scoped; // installs glGenTextures/glBindTexture mocks + resets caches + MobileGL::Vector deleted; + g_deletedTextureIds = &deleted; + auto functions = g_GLESFuncs; + functions.glDeleteTextures = SG_DeleteTextures; + SetGLESFuncsTable(functions); + + const auto texture2DSlot = static_cast(MobileGL::TextureTarget::Texture2D); + GLuint id = 0; + { + auto backendTexture = MobileGL::MakeShared(); + id = backendTexture->GetBackendTextureId(); + ASSERT_NE(id, 0u); + backendTexture->Bind(GL_TEXTURE_2D, 0); + ASSERT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], backendTexture.get()); + } + // Frontend glDeleteTextures used to leak the backend id forever and leave the + // cache pointer dangling (heap-address reuse then false-skips a later Bind). + ASSERT_EQ(deleted.size(), 1u); + EXPECT_EQ(deleted[0], id); + EXPECT_EQ(TextureImpl::g_boundTexturesCache[0][texture2DSlot], nullptr); + + // A wrapper whose context died must NOT delete a foreign (recycled) name. + { + auto backendTexture = MobileGL::MakeShared(); + ++TextureImpl::g_textureContextGeneration; + backendTexture.reset(); + --TextureImpl::g_textureContextGeneration; // restore for later tests + EXPECT_EQ(deleted.size(), 1u); + } + g_deletedTextureIds = nullptr; +}