diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 93046299..341e6bff 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -117,6 +117,13 @@ namespace MobileGL::MG_Config { // per-draw glBufferSubData path instead of the persistent-mapped ring allocator // (negative control / driver-bug escape hatch). Bool DisableUboRing = false; + // MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES + // depth/stencil reads and always go through the shader-sampling emulation. Core GL + // ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does, + // Adreno does not), which means the emulation is dead code on exactly the stack the + // headless suite runs on. This forces it live so the scenarios and the CTS can + // exercise the path, and gives the device an A/B lever over the same choice. + Bool EsprytForceDepthStencilReadbackEmulation = false; // MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws, // texture-name reuse after delete) even on contexts that explicitly requested a core // profile. Without it, relaxed semantics still apply to every context that did not diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2322e8e4..e37dabfe 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -176,6 +176,8 @@ namespace MobileGL::MG_ConfigLoader { features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); + features.EsprytForceDepthStencilReadbackEmulation = + QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN"); features.MagmaDisableBlendedDepthWriteQuirk = diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d5130cd9..1cbf6658 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3859,6 +3859,171 @@ namespace MobileGL::MG_Backend::DirectGLES { return resolved; } + // --------------------------------------------------------------------------------- + // Shared guard for the emulation passes that have to DRAW to get their work done: the + // single-sample -> multisample blit replicate below, and the depth/stencil readback + // emulation further down. Both borrow the application's ES context for a full-screen + // pass, so everything they disturb is captured here and put back on the way out - the + // sync layer's shadow of the driver state has to stay true, and one leaked binding + // regresses every draw that follows. + // + // Three members of that set are not obvious: + // + // - The borrowed texture unit. Both passes bind their scratch texture to + // TextureImpl::TempTextureUnit, and the unit's binding is only restored correctly by + // asking TextureImpl::g_boundTexturesCache what is supposed to be there: the raw + // glBindTexture the passes issue never moves that cache, so restoring a *queried* + // id leaves the driver and the cache disagreeing and the per-draw binding memo + // false-skips the re-bind - the borrowed-slot failure mode that showed up as + // process-wide glyph death under Iris. Reading GL_TEXTURE_BINDING_2D was doubly + // wrong here because the replicate path may already have switched units and bound + // its own scratch texture by the time it asked. + // + // - The sampler object on that unit. It would override the scratch texture's own + // filter and compare parameters. Unbinding through SamplerImpl::UnbindSampler moves + // the sampler cache, which is exactly what re-opens BindCurrentUnitSamplers' memo, + // so the application's sampler comes back on the next draw with no explicit restore. + // + // - An active transform feedback capture. GL rejects a draw issued with a program + // other than the one that began the capture, and would otherwise append the + // emulation's vertices to the application's buffers. + class ScopedEmulationDrawState { + public: + ScopedEmulationDrawState() { + g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &m_program); + g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &m_vertexArray); + g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, m_viewport); + g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, m_scissorBox); + g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, m_colorMask); + g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &m_depthFunc); + g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &m_depthMask); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &m_stencilFunc[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &m_stencilFunc[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &m_stencilRef[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &m_stencilRef[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &m_stencilValueMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &m_stencilValueMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &m_stencilWriteMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &m_stencilWriteMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &m_stencilFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &m_stencilFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &m_stencilDepthFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &m_stencilDepthFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &m_stencilPass[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &m_stencilPass[1]); + for (CapabilityState& capability : m_capabilities) { + capability.enabled = g_GLESFuncs.glIsEnabled(capability.cap); + } + // GL_SAMPLE_MASK is ES 3.1; on an older driver the query above just raised + // GL_INVALID_ENUM and answered GL_FALSE, which is also the right thing to + // restore. Drop the flag so it is not misattributed to the emulation's own work. + DrainBlitErrors(); + + if (MG_State::pGLContext->IsTransformFeedbackActive() && + !MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) { + g_GLESFuncs.glPauseTransformFeedback(); + m_pausedTransformFeedback = true; + DrainBlitErrors(); + } + + m_activeTextureUnit = TextureImpl::g_activeTextureUnit; + TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit); + SamplerImpl::UnbindSampler(TextureImpl::TempTextureUnit); + + // The neutral baseline every emulation pass wants: nothing culled, nothing + // clipped, nothing tested, no coverage games, and colour writes open. Callers + // turn back on only what they need (the replicate pass wants the depth and + // stencil tests, and masks colour off because it writes neither). + for (const CapabilityState& capability : m_capabilities) { + g_GLESFuncs.glDisable(capability.cap); + } + g_GLESFuncs.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + g_GLESFuncs.glDepthMask(GL_FALSE); + DrainBlitErrors(); + } + + ~ScopedEmulationDrawState() { + g_GLESFuncs.glUseProgram(static_cast(m_program)); + // Put back whatever the binding cache says lives on the borrowed unit, not what + // the driver happened to hold: see the class comment. + auto* cachedBound = + TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit] + [static_cast(TextureTarget::Texture2D)]; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0); + TextureImpl::ActivateTextureUnit(m_activeTextureUnit); + VertexArrayImpl::BindBackendVAOId(static_cast(m_vertexArray)); + g_GLESFuncs.glViewport(m_viewport[0], m_viewport[1], m_viewport[2], m_viewport[3]); + g_GLESFuncs.glScissor(m_scissorBox[0], m_scissorBox[1], m_scissorBox[2], m_scissorBox[3]); + g_GLESFuncs.glColorMask(m_colorMask[0], m_colorMask[1], m_colorMask[2], m_colorMask[3]); + g_GLESFuncs.glDepthFunc(static_cast(m_depthFunc)); + g_GLESFuncs.glDepthMask(m_depthMask); + const GLenum faces[2] = {GL_FRONT, GL_BACK}; + for (SizeT face = 0; face < 2; ++face) { + g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast(m_stencilFunc[face]), + m_stencilRef[face], + static_cast(m_stencilValueMask[face])); + g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast(m_stencilFail[face]), + static_cast(m_stencilDepthFail[face]), + static_cast(m_stencilPass[face])); + g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast(m_stencilWriteMask[face])); + } + for (const CapabilityState& capability : m_capabilities) { + if (capability.enabled) { + g_GLESFuncs.glEnable(capability.cap); + } else { + g_GLESFuncs.glDisable(capability.cap); + } + } + // The per-draw-buffer colour masks are not covered by the non-indexed + // glColorMask above. + for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { + const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; + if (g_GLESFuncs.glColorMaski) { + g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE, + colorMask.y() ? GL_TRUE : GL_FALSE, colorMask.z() ? GL_TRUE : GL_FALSE, + colorMask.w() ? GL_TRUE : GL_FALSE); + } + } + if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) { + g_GLESFuncs.glResumeTransformFeedback(); + } + DrainBlitErrors(); + } + + ScopedEmulationDrawState(const ScopedEmulationDrawState&) = delete; + ScopedEmulationDrawState& operator=(const ScopedEmulationDrawState&) = delete; + + private: + struct CapabilityState { + GLenum cap; + GLboolean enabled; + }; + + GLint m_program = 0; + GLint m_vertexArray = 0; + GLint m_viewport[4] = {0, 0, 0, 0}; + GLint m_scissorBox[4] = {0, 0, 0, 0}; + GLboolean m_colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; + GLint m_depthFunc = GL_LESS; + GLboolean m_depthMask = GL_TRUE; + GLint m_stencilFunc[2] = {GL_ALWAYS, GL_ALWAYS}; + GLint m_stencilRef[2] = {0, 0}; + GLint m_stencilValueMask[2] = {~0, ~0}; + GLint m_stencilWriteMask[2] = {~0, ~0}; + GLint m_stencilFail[2] = {GL_KEEP, GL_KEEP}; + GLint m_stencilDepthFail[2] = {GL_KEEP, GL_KEEP}; + GLint m_stencilPass[2] = {GL_KEEP, GL_KEEP}; + Uint m_activeTextureUnit = 0; + Bool m_pausedTransformFeedback = false; + CapabilityState m_capabilities[10] = { + {GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE}, + {GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE}, + {GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE}, + {GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE}, + {GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE}, + }; + }; + // --------------------------------------------------------------------------------- // Single-sample -> multisample blit ("replicate") // @@ -3987,40 +4152,47 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - // Sized internal format of the read framebuffer's depth (or, failing that, stencil) - // attachment. The scratch copy has to use the very same one: ES rejects a - // depth/stencil blit between differing formats even when both sides are single-sampled. + // Sized internal format of ONE attachment point of the bound READ framebuffer, or 0 + // when there is no object there to ask (the default framebuffer's buffers have no + // queryable format at all). The scratch copy has to use the very same one: ES rejects + // a depth/stencil blit between differing formats even when both sides are + // single-sampled. + static GLenum QueryAttachmentSizedFormat(GLenum attachment) { + GLint objectType = 0; + GLint objectName = 0; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName); + if (objectName == 0) { + return 0; + } + GLint internalFormat = 0; + if (objectType == GL_RENDERBUFFER) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(objectName)); + g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + } else if (objectType == GL_TEXTURE) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(objectName)); + g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previous)); + } + return static_cast(internalFormat); + } + + // The read framebuffer's depth format, or - when it has no depth - its stencil one. static GLenum QueryReadDepthStencilFormat(GLenum* outAttachment) { const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT}; for (const GLenum attachment : attachments) { - GLint objectType = 0; - GLint objectName = 0; - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); - g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, - GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName); - if (objectName == 0) { - continue; - } - GLint internalFormat = 0; - if (objectType == GL_RENDERBUFFER) { - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(objectName)); - g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); - } else if (objectType == GL_TEXTURE) { - GLint previous = 0; - g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(objectName)); - g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, - &internalFormat); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previous)); - } + const GLenum internalFormat = QueryAttachmentSizedFormat(attachment); if (internalFormat != 0) { if (outAttachment) *outAttachment = attachment; - return static_cast(internalFormat); + return internalFormat; } } return 0; @@ -4088,8 +4260,14 @@ namespace MobileGL::MG_Backend::DirectGLES { GLint previousRead = 0; g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); - GLint previousActiveTexture = 0; - g_GLESFuncs.glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture); + + // Everything below borrows the application's context - a texture unit for the + // scratch sampling and the whole rasterization pipeline for the replicate passes - + // so the guard is taken before the first of those, not just before the draws. It + // also puts the scissor test where the staging blit below needs it: a blit is + // scissored like a draw, and the application's box would otherwise clip the copy + // into the scratch texture. + ScopedEmulationDrawState emulationState; // Copy the source rectangle into a scratch texture of its own format: both sides of // that blit are single-sampled, which ES does allow. @@ -4106,7 +4284,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (s_texture == 0) { ok = false; } else { - g_GLESFuncs.glActiveTexture(GL_TEXTURE0); + // The guard already activated TempTextureUnit and cleared its sampler. g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); DrainBlitErrors(); g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, sourceFormat, s_textureWidth, s_textureHeight); @@ -4138,67 +4316,10 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); FramebufferImpl::InvalidateFramebufferBindingCache(); if (!ok) { - g_GLESFuncs.glActiveTexture(static_cast(previousActiveTexture)); MGLOG_E("BlitFramebuffer: could not stage the source for the multisample replicate"); return false; } - // Everything below draws into the caller's multisample draw framebuffer, so the - // pipeline state it depends on is saved and put back byte for byte - the sync layer's - // shadow of the driver state has to stay true. - GLint previousProgram = 0; - GLint previousVertexArray = 0; - GLint previousTexture = 0; - GLint previousViewport[4] = {0, 0, 0, 0}; - GLint previousScissorBox[4] = {0, 0, 0, 0}; - GLboolean previousColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; - GLint previousDepthFunc = GL_LESS; - GLboolean previousDepthMask = GL_TRUE; - GLint previousStencilFunc[2] = {GL_ALWAYS, GL_ALWAYS}; - GLint previousStencilRef[2] = {0, 0}; - GLint previousStencilValueMask[2] = {~0, ~0}; - GLint previousStencilWriteMask[2] = {~0, ~0}; - GLint previousStencilFail[2] = {GL_KEEP, GL_KEEP}; - GLint previousStencilDepthFail[2] = {GL_KEEP, GL_KEEP}; - GLint previousStencilPass[2] = {GL_KEEP, GL_KEEP}; - g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &previousProgram); - g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previousVertexArray); - g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTexture); - g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, previousViewport); - g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, previousScissorBox); - g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, previousColorMask); - g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &previousDepthFunc); - g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &previousDepthMask); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &previousStencilFunc[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &previousStencilFunc[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &previousStencilRef[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &previousStencilRef[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &previousStencilValueMask[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &previousStencilValueMask[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &previousStencilWriteMask[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &previousStencilWriteMask[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &previousStencilFail[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &previousStencilFail[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &previousStencilDepthFail[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &previousStencilDepthFail[1]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &previousStencilPass[0]); - g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &previousStencilPass[1]); - - struct CapabilityState { - GLenum cap; - GLboolean enabled; - }; - CapabilityState capabilities[] = { - {GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE}, - {GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE}, - {GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE}, - {GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE}, - {GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE}, - }; - for (CapabilityState& capability : capabilities) { - capability.enabled = g_GLESFuncs.glIsEnabled(capability.cap); - } - const GLint dstLeft = std::min(dstX0, dstX1); const GLint dstBottom = std::min(dstY0, dstY1); const Bool mirrorX = (srcX1 > srcX0) != (dstX1 > dstX0); @@ -4210,19 +4331,15 @@ namespace MobileGL::MG_Backend::DirectGLES { const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY, mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f}; + // The guard already left the pipeline neutral (nothing culled, tested, blended or + // coverage-masked) on the borrowed texture unit; what is left is this pass's own + // choices - the destination rectangle, and colour writes off because it writes only + // depth and stencil. VertexArrayImpl::BindBackendVAOId(s_vertexArray); - g_GLESFuncs.glActiveTexture(GL_TEXTURE0); g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight); g_GLESFuncs.glScissor(dstLeft, dstBottom, dstWidth, dstHeight); g_GLESFuncs.glEnable(GL_SCISSOR_TEST); - g_GLESFuncs.glDisable(GL_CULL_FACE); - g_GLESFuncs.glDisable(GL_BLEND); - g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD); - g_GLESFuncs.glDisable(GL_POLYGON_OFFSET_FILL); - g_GLESFuncs.glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); - g_GLESFuncs.glDisable(GL_SAMPLE_COVERAGE); - g_GLESFuncs.glDisable(GL_SAMPLE_MASK); g_GLESFuncs.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); DrainBlitErrors(); @@ -4266,44 +4383,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool replicated = g_GLESFuncs.glGetError() == GL_NO_ERROR; - g_GLESFuncs.glUseProgram(static_cast(previousProgram)); - g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previousTexture)); - g_GLESFuncs.glActiveTexture(static_cast(previousActiveTexture)); - VertexArrayImpl::BindBackendVAOId(static_cast(previousVertexArray)); - g_GLESFuncs.glViewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]); - g_GLESFuncs.glScissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], - previousScissorBox[3]); - g_GLESFuncs.glColorMask(previousColorMask[0], previousColorMask[1], previousColorMask[2], - previousColorMask[3]); - g_GLESFuncs.glDepthFunc(static_cast(previousDepthFunc)); - g_GLESFuncs.glDepthMask(previousDepthMask); - const GLenum faces[2] = {GL_FRONT, GL_BACK}; - for (SizeT face = 0; face < 2; ++face) { - g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast(previousStencilFunc[face]), - previousStencilRef[face], - static_cast(previousStencilValueMask[face])); - g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast(previousStencilFail[face]), - static_cast(previousStencilDepthFail[face]), - static_cast(previousStencilPass[face])); - g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast(previousStencilWriteMask[face])); - } - for (const CapabilityState& capability : capabilities) { - if (capability.enabled) { - g_GLESFuncs.glEnable(capability.cap); - } else { - g_GLESFuncs.glDisable(capability.cap); - } - } - // The per-draw-buffer colour masks are not covered by the non-indexed glColorMask above. - for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { - const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; - if (g_GLESFuncs.glColorMaski) { - g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE, - colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE); - } - } - DrainBlitErrors(); - + // Everything the pass disturbed goes back through emulationState's destructor. if (!replicated) { MGLOG_E("BlitFramebuffer: multisample replicate fallback failed"); } @@ -5755,6 +5835,536 @@ namespace MobileGL::MG_Backend::DirectGLES { return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); } + // Destination walk shared by the depth, stencil and packed depth-stencil readbacks. + // Each of those produces its rows tightly packed and has to land them in the caller's + // destination - client memory or the bound pixel-pack buffer - under the PACK + // pixel-store parameters. `fillRow` is handed the row index and a buffer of exactly one + // packed row to populate. Only real pixel rows are written, so PACK skip/row-length gap + // regions stay untouched. + template + static Bool StoreReadbackRowsToClient(GLsizei width, GLsizei height, SizeT dstPixelBytes, void* pixels, + const char* what, FillRow&& fillRow) { + 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 dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; + const SizeT rowBytes = static_cast(width) * dstPixelBytes; + const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + rowBytes; + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + const SizeT pboOffset = reinterpret_cast(pixels); + if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("ReadPixels: %s readback PBO is too small", what); + return false; + } + Vector rowBuf(rowBytes); + for (GLsizei row = 0; row < height; ++row) { + fillRow(row, rowBuf.data()); + const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend({rowBuf.data(), rowBytes}, pboOffset + rowOffset); + } else if (pixels != nullptr) { + Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), rowBytes); + } + } + if (pixelPackBufferObject) { + // WritebackFromBackend bumps change serials with no backend op; re-open the + // buffer draw-clean memos (once for the whole row loop). + BufferImpl::BumpBufferMutationEpoch(); + } + return true; + } + + // A normalized depth scaled into the full range of an unsigned integer of `maxValue` + // (GL 4.6 core table 18.2), without ever rounding past the top of that range. + static Uint32 NormalizedDepthToUnsigned(Float depth, Double maxValue) { + const Double clamped = std::min(std::max(static_cast(depth), 0.0), 1.0); + const Double scaled = clamped * maxValue + 0.5; + return static_cast(scaled >= maxValue ? maxValue : scaled); + } + + // --------------------------------------------------------------------------------- + // Depth / stencil readback by shader sampling + // + // Desktop GL reads depth and stencil back through glReadPixels; ES has no such call at + // all. GL_DEPTH_COMPONENT, GL_STENCIL_INDEX and GL_DEPTH_STENCIL are simply not + // accepted formats there, and the optional extensions that add them (GL_NV_read_depth, + // GL_NV_read_stencil, GL_NV_read_depth_stencil) are absent on both the Adreno device + // and Mesa's ES. Every native attempt therefore failed with a GL error and wrote + // NOTHING, so the caller kept whatever its buffer already held - which is how the CTS + // reports "expected DEPTH[0.25] but got DEPTH[0.2]": 0.2 is the poison value the test + // itself put there. + // + // What ES *can* do is sample a depth texture, so the emulation goes the long way round: + // + // 1. Stage. glBlitFramebuffer the requested rectangle out of the bound READ + // framebuffer into a scratch depth(-stencil) TEXTURE of the very same sized + // internal format. One staging copy serves every source kind uniformly - a + // texture attachment of any target/level/layer, a renderbuffer (not samplable at + // all), the default framebuffer, and a multisample attachment (the blit resolves + // it on the way). ES rejects a depth/stencil blit between differing formats, so + // the scratch has to match the source exactly; see DescribeReadDepthStencilSource. + // 2. Convert. Draw a full-screen triangle that samples the staged texture into a + // scratch R32UI colour target: depth as floatBitsToUint (bit-exact for every depth + // format, and an integer colour target needs no float-renderable extension), + // stencil through GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX. + // 3. Read. glReadPixels the colour target with GL_RGBA_INTEGER / GL_UNSIGNED_INT - + // the pair ES guarantees for an unsigned-integer attachment - and hand the values + // to the existing re-encoders, which already own the client-side (format, type) + // layout and the PACK pixel-store parameters. + // + // Both scratch images hold the rectangle at their own origin and the pass runs with a + // matching viewport, so GL's bottom-up row order survives untouched: scratch row 0 is + // source row `y`, which is exactly the first row glReadPixels(x, y, ...) owes the + // caller. No flip anywhere. + namespace DepthStencilSamplingReadImpl { + static Uint s_contextGeneration = ~0u; + static GLuint s_colorFramebuffer = 0; + static GLuint s_colorTexture = 0; + static GLsizei s_colorWidth = 0; + static GLsizei s_colorHeight = 0; + static GLuint s_vertexArray = 0; + static GLuint s_depthProgram = 0; + static GLuint s_stencilProgram = 0; + static GLint s_depthUvTransform = -1; + static GLint s_stencilUvTransform = -1; + static Bool s_programsFailed = false; + + // One staging slot per aspect. A framebuffer is allowed to carry its depth and its + // stencil in two DIFFERENT objects with two different formats - the framebuffer_blit + // cases pair a DEPTH_COMPONENT* attachment with a separate STENCIL_INDEX8 one - and + // the two aspects are staged independently for exactly that reason. A single shared + // slot would also throw its immutable storage away and re-create it on every + // alternation between the two. + struct StageSlot { + // A framebuffer of its own, not a shared one. GL_DEPTH_STENCIL_ATTACHMENT sets + // the depth AND the stencil point, so a packed scratch staged for one aspect + // would leave the other aspect's point pointing at it; the next stage of that + // other aspect attaches its own (differently formatted) texture to its own point + // and the framebuffer is then incomplete - which reads as "no candidate format + // worked" and writes nothing at all. + GLuint framebuffer = 0; + GLuint texture = 0; + GLenum format = 0; + GLsizei width = 0; + GLsizei height = 0; + // The default framebuffer has no queryable internal format, so the one that turns + // out to be blit-compatible is remembered: it cannot change for the life of the + // context, and re-probing it on every read would cost a failed blit each time. + GLenum defaultFramebufferFormat = 0; + }; + static StageSlot s_slots[2]; // [0] depth, [1] stencil + + // `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in + // the fragment language, which is allowed to be 16 bits - it would saw the top half + // off every depth bit pattern and every stencil fetch. + static const char* const kDepthFetchFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp int;\n" + "precision highp sampler2D;\n" + "uniform sampler2D uSource;\n" + "in vec2 vUv;\n" + "layout(location = 0) out uvec4 oBits;\n" + "void main() {\n" + " oBits = uvec4(floatBitsToUint(texture(uSource, vUv).r), 0u, 0u, 0u);\n" + "}\n"; + + static const char* const kStencilFetchFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp int;\n" + "precision highp usampler2D;\n" + "uniform usampler2D uSource;\n" + "in vec2 vUv;\n" + "layout(location = 0) out uvec4 oBits;\n" + "void main() {\n" + " oBits = uvec4(texture(uSource, vUv).r, 0u, 0u, 0u);\n" + "}\n"; + + static Bool EnsureResources() { + if (s_contextGeneration != g_backendContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_colorFramebuffer = 0; + s_colorTexture = 0; + s_colorWidth = 0; + s_colorHeight = 0; + s_vertexArray = 0; + s_depthProgram = 0; + s_stencilProgram = 0; + s_programsFailed = false; + s_slots[0] = StageSlot{}; + s_slots[1] = StageSlot{}; + s_contextGeneration = g_backendContextGeneration; + } + if (s_programsFailed) { + return false; + } + if (s_depthProgram == 0) { + // Same full-screen vertex shader (and its uUvTransform) as the replicate + // blit: a staging slot is sized to the largest rectangle seen so far, so the + // quad's [0,1] coordinates have to be scaled down to the part it occupies. + s_depthProgram = ReplicateBlitImpl::BuildProgram(kDepthFetchFragmentSource); + s_stencilProgram = ReplicateBlitImpl::BuildProgram(kStencilFetchFragmentSource); + if (s_depthProgram == 0 || s_stencilProgram == 0) { + s_programsFailed = true; + MGLOG_E("ReadPixels: could not build the depth/stencil readback programs"); + return false; + } + s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); + s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); + } + for (StageSlot& slot : s_slots) { + if (slot.framebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &slot.framebuffer); + if (slot.framebuffer == 0) return false; + } + } + if (s_colorFramebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_colorFramebuffer); + if (s_colorFramebuffer == 0) return false; + } + if (s_vertexArray == 0) { + g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); + if (s_vertexArray == 0) return false; + } + return true; + } + + // Ordered guesses at the sized internal format backing one aspect. More than one + // entry only when the source's own format cannot be queried (the default + // framebuffer), in which case the reported channel sizes narrow it down and the + // staging blit picks the winner by being the only one that raises no GL error. + struct AspectCandidates { + GLenum formats[4] = {0, 0, 0, 0}; + Uint count = 0; + void Push(GLenum format) { + for (Uint i = 0; i < count; ++i) { + if (formats[i] == format) return; + } + if (count < 4) formats[count++] = format; + } + }; + + static GLenum AttachmentPointFor(Bool isDefault, Bool stencilAspect) { + // The default framebuffer names its buffers GL_DEPTH / GL_STENCIL; a user + // framebuffer names them GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT, and asking + // one for the other's spelling is GL_INVALID_ENUM. + if (isDefault) return stencilAspect ? GL_STENCIL : GL_DEPTH; + return stencilAspect ? GL_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT; + } + + // Returns false when the bound READ framebuffer has no such aspect at all. + static Bool DescribeAspect(Bool stencilAspect, Bool* outIsDefault, AspectCandidates* out) { + GLint readFramebuffer = 0; + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); + const Bool isDefault = readFramebuffer == 0; + if (outIsDefault) *outIsDefault = isDefault; + const GLenum point = AttachmentPointFor(isDefault, stencilAspect); + + ClearGLErrors(); + GLint objectType = GL_NONE; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, point, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); + ClearGLErrors(); + if (objectType == GL_NONE) { + return false; + } + + if (!isDefault) { + // A real object: ask it directly. That answer is certainly blit-compatible, + // so it is used on its own. The query binds the attachment as GL_TEXTURE_2D, + // which an array or cube attachment refuses - it answers 0, the size-derived + // guesses below take over, and the refusal must not be left on the error + // queue for the caller's next glGetError to pick up as its own. + const GLenum exact = ReplicateBlitImpl::QueryAttachmentSizedFormat(point); + ClearGLErrors(); + if (exact != 0) { + out->Push(exact); + return true; + } + } else if (s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat != 0) { + out->Push(s_slots[stencilAspect ? 1 : 0].defaultFramebufferFormat); + } + + // Nothing to ask (or the query failed): rebuild plausible sized formats from the + // channel sizes the attachment points report. The OTHER aspect's size matters as + // much as this one's - a depth buffer that also carries stencil has to be staged + // into a packed scratch, because ES only blits depth between identical formats. + GLint depthBits = 0; + GLint stencilBits = 0; + GLint componentType = GL_UNSIGNED_NORMALIZED; + ClearGLErrors(); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, + AttachmentPointFor(isDefault, false), + GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, + AttachmentPointFor(isDefault, true), + GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv( + GL_READ_FRAMEBUFFER, AttachmentPointFor(isDefault, false), + GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType); + ClearGLErrors(); + + const Bool floatDepth = componentType == GL_FLOAT; + const Bool packed = depthBits > 0 && stencilBits > 0; + if (stencilAspect) { + if (packed) { + out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); + out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); + } + out->Push(GL_STENCIL_INDEX8); + if (!packed) { + out->Push(GL_DEPTH24_STENCIL8); + } + } else if (packed) { + out->Push(floatDepth ? GL_DEPTH32F_STENCIL8 : GL_DEPTH24_STENCIL8); + out->Push(floatDepth ? GL_DEPTH24_STENCIL8 : GL_DEPTH32F_STENCIL8); + } else if (floatDepth) { + out->Push(GL_DEPTH_COMPONENT32F); + out->Push(GL_DEPTH32F_STENCIL8); + } else if (depthBits > 0 && depthBits <= 16) { + out->Push(GL_DEPTH_COMPONENT16); + out->Push(GL_DEPTH_COMPONENT24); + } else { + out->Push(GL_DEPTH_COMPONENT24); + out->Push(GL_DEPTH24_STENCIL8); + out->Push(GL_DEPTH_COMPONENT32F); + } + return out->count != 0; + } + + // Point a staging slot at `format`, growing it if the rectangle needs it, and leave + // it bound on the borrowed texture unit. + static Bool EnsureStageTexture(StageSlot& slot, GLenum format, GLsizei width, GLsizei height) { + if (slot.texture != 0 && slot.format == format && slot.width >= width && slot.height >= height) { + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + return true; + } + if (slot.texture != 0) { + g_GLESFuncs.glDeleteTextures(1, &slot.texture); // immutable storage cannot be resized + ScratchFBOImpl::NoteTextureIdDeleted(slot.texture); + slot.texture = 0; + } + slot.format = 0; + slot.width = std::max(slot.width, width); + slot.height = std::max(slot.height, height); + g_GLESFuncs.glGenTextures(1, &slot.texture); + if (slot.texture == 0) return false; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + ClearGLErrors(); + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, format, slot.width, slot.height); + const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + // A depth texture left in compare mode samples to 0/1 instead of the stored value. + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); + ClearGLErrors(); + slot.format = ok ? format : 0; + return ok; + } + + static Bool EnsureColorTexture(GLsizei width, GLsizei height) { + if (s_colorTexture != 0 && s_colorWidth >= width && s_colorHeight >= height) { + return true; + } + if (s_colorTexture != 0) { + g_GLESFuncs.glDeleteTextures(1, &s_colorTexture); + ScratchFBOImpl::NoteTextureIdDeleted(s_colorTexture); + s_colorTexture = 0; + } + s_colorWidth = std::max(s_colorWidth, width); + s_colorHeight = std::max(s_colorHeight, height); + g_GLESFuncs.glGenTextures(1, &s_colorTexture); + if (s_colorTexture == 0) return false; + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_colorTexture); + ClearGLErrors(); + // R32UI is colour-renderable in ES 3.0 core - no float-renderability extension + // needed - and carries a depth bit pattern or a stencil index without loss. + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, s_colorWidth, s_colorHeight); + const Bool ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ClearGLErrors(); + if (!ok) { + s_colorWidth = 0; + s_colorHeight = 0; + } + return ok; + } + + // Copy one aspect of the requested rectangle out of the bound READ framebuffer into + // its staging slot, trying each candidate format until one is blit-compatible. + // Requires the slot's own framebuffer bound as DRAW. + static Bool StageAspect(StageSlot& slot, const AspectCandidates& candidates, Bool stencilAspect, Bool isDefault, + GLint x, GLint y, GLsizei width, GLsizei height) { + const GLbitfield aspectBit = stencilAspect ? GL_STENCIL_BUFFER_BIT : GL_DEPTH_BUFFER_BIT; + for (Uint candidate = 0; candidate < candidates.count; ++candidate) { + const GLenum format = candidates.formats[candidate]; + const Bool formatHasAspect = stencilAspect ? ReplicateBlitImpl::FormatHasStencil(format) + : ReplicateBlitImpl::FormatHasDepth(format); + if (!formatHasAspect) { + continue; + } + if (!EnsureStageTexture(slot, format, width, height)) { + continue; + } + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, + ReplicateBlitImpl::ScratchAttachmentFor(format), GL_TEXTURE_2D, + slot.texture, 0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + continue; + } + ClearGLErrors(); + // Only this aspect: a packed scratch standing in for a separate attachment + // has a second half with nothing to copy into it. + g_GLESFuncs.glBlitFramebuffer(x, y, x + width, y + height, 0, 0, width, height, aspectBit, GL_NEAREST); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + continue; + } + if (isDefault) { + slot.defaultFramebufferFormat = format; + } + return true; + } + return false; + } + + // Runs one conversion pass over a staged slot and reads its colour target back. + // Requires the slot's texture bound on the borrowed unit and s_colorFramebuffer + // bound as DRAW. + static Bool ConvertAndRead(const StageSlot& slot, Bool stencilAspect, GLsizei width, GLsizei height, + Vector& outWords) { + if (ReplicateBlitImpl::FormatHasDepth(slot.format) && ReplicateBlitImpl::FormatHasStencil(slot.format)) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, + stencilAspect ? GL_STENCIL_INDEX : GL_DEPTH_COMPONENT); + } + const Float uvScaleX = static_cast(width) / static_cast(slot.width); + const Float uvScaleY = static_cast(height) / static_cast(slot.height); + g_GLESFuncs.glUseProgram(stencilAspect ? s_stencilProgram : s_depthProgram); + g_GLESFuncs.glUniform4f(stencilAspect ? s_stencilUvTransform : s_depthUvTransform, uvScaleX, uvScaleY, + 0.0f, 0.0f); + g_GLESFuncs.glViewport(0, 0, width, height); + ClearGLErrors(); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + if (g_GLESFuncs.glGetError() != GL_NO_ERROR) { + MGLOG_E("ReadPixels: the %s conversion pass failed", stencilAspect ? "stencil" : "depth"); + return false; + } + + // GL_RGBA_INTEGER / GL_UNSIGNED_INT is the pair ES guarantees for an unsigned + // integer attachment whatever its channel count, so four words come back per + // pixel and only the first carries anything. + outWords.assign(static_cast(width) * static_cast(height) * 4u, 0u); + ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false); + FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, s_colorFramebuffer); + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0}); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, outWords.data()); + const GLenum readError = g_GLESFuncs.glGetError(); + if (readError != GL_NO_ERROR) { + MGLOG_E("ReadPixels: could not read the %s conversion target back: %s", + stencilAspect ? "stencil" : "depth", MG_Util::ConvertGLEnumToString(readError).c_str()); + return false; + } + return true; + } + + // Stage, convert and read one aspect. The caller owns the state guard and the DRAW + // framebuffer scope. + static Bool ReadAspect(Bool stencilAspect, GLint x, GLint y, GLsizei width, GLsizei height, + Vector& outWords) { + Bool isDefault = false; + AspectCandidates candidates; + if (!DescribeAspect(stencilAspect, &isDefault, &candidates)) { + return false; + } + StageSlot& slot = s_slots[stencilAspect ? 1 : 0]; + + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer); + if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) { + MGLOG_E("ReadPixels: no ES-compatible scratch format for the %s source", + stencilAspect ? "stencil" : "depth"); + return false; + } + + if (!EnsureColorTexture(width, height)) { + MGLOG_E("ReadPixels: could not allocate the depth/stencil conversion target"); + return false; + } + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, s_colorFramebuffer); + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + s_colorTexture, 0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E("ReadPixels: the depth/stencil conversion target is not renderable"); + return false; + } + + // EnsureColorTexture may have taken the borrowed unit for its own storage call. + VertexArrayImpl::BindBackendVAOId(s_vertexArray); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, slot.texture); + return ConvertAndRead(slot, stencilAspect, width, height, outWords); + } + + // Fills whichever of the two outputs the caller asked for from the bound READ + // framebuffer. Returns false when the emulation could not service the request at + // all, leaving the caller to report the failure the way it always has. + static Bool Read(GLint x, GLint y, GLsizei width, GLsizei height, Vector* outDepth, + Vector* outStencil) { + if (width <= 0 || height <= 0 || (outDepth == nullptr && outStencil == nullptr)) { + return false; + } + // Sampling the stencil half of a packed texture goes through + // GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state; on an older driver the + // pname would just raise GL_INVALID_ENUM and the shader would read depth bits as + // stencil. + const Bool supportsStencilTextureMode = + g_GLESCapabilities.GLESVersion.Major > 3 || + (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); + if (outStencil != nullptr && !supportsStencilTextureMode) { + return false; + } + if (!EnsureResources()) { + return false; + } + + // Taken before the first scratch texture bind: the guard owns the borrowed + // texture unit as well as the pipeline, and it is what puts the scissor test out + // of the way of the staging blit. + ScopedEmulationDrawState emulationState; + ScopedFramebufferBinding drawBinding(/*saveRead=*/false, /*saveDraw=*/true); + + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector words; + if (outDepth != nullptr) { + if (!ReadAspect(/*stencilAspect=*/false, x, y, width, height, words)) { + return false; + } + outDepth->assign(pixelCount, 0.0f); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint32 bits = words[i * 4u]; + Float value = 0.0f; + Memcpy(&value, &bits, sizeof(value)); + (*outDepth)[i] = value; + } + } + if (outStencil != nullptr) { + if (!ReadAspect(/*stencilAspect=*/true, x, y, width, height, words)) { + return false; + } + outStencil->assign(pixelCount, 0); + for (SizeT i = 0; i < pixelCount; ++i) { + (*outStencil)[i] = static_cast(words[i * 4u] & 0xFFu); + } + } + return true; + } + } // namespace DepthStencilSamplingReadImpl + // One normalized depth value per pixel, tightly packed. Which native read a driver // accepts depends on the attached format: a fixed-point depth buffer takes // GL_UNSIGNED_INT, while a floating-point one (DEPTH_COMPONENT32F, @@ -5764,76 +6374,89 @@ namespace MobileGL::MG_Backend::DirectGLES { outDepth.assign(static_cast(width) * static_cast(height), 0.0f); ScopedPixelPackBuffer packBuffer(0); ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); - Vector raw(outDepth.size()); - // 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()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - for (SizeT i = 0; i < outDepth.size(); ++i) { - outDepth[i] = static_cast(static_cast(raw[i]) / 4294967295.0); + GLenum floatError = GL_INVALID_OPERATION; + if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { + Vector raw(outDepth.size()); + // 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()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + for (SizeT i = 0; i < outDepth.size(); ++i) { + outDepth[i] = static_cast(static_cast(raw[i]) / 4294967295.0); + } + return true; } + + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, outDepth.data()); + floatError = g_GLESFuncs.glGetError(); + if (floatError == GL_NO_ERROR) { + return true; + } + } + + // Neither native spelling exists on this driver (which is the ordinary case: ES has + // no depth readback in core and GL_NV_read_depth is rare), so sample the attachment + // instead. The scoped pack state above is irrelevant to that path - it reads its own + // scratch colour target - but harmless, and leaving it in place keeps the restore in + // one place. + if (DepthStencilSamplingReadImpl::Read(x, y, width, height, &outDepth, /*outStencil=*/nullptr)) { return true; } - - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, outDepth.data()); - const GLenum floatError = g_GLESFuncs.glGetError(); - if (floatError != GL_NO_ERROR) { - MGLOG_E("ReadPixels: neither GL_UNSIGNED_INT nor GL_FLOAT depth readback is available: %s", - MG_Util::ConvertGLEnumToString(floatError).c_str()); - return false; - } - return true; + MGLOG_E("ReadPixels: no depth readback path is available: native reads failed with %s and the " + "sampling emulation could not service the source", + MG_Util::ConvertGLEnumToString(floatError).c_str()); + return false; } - static Bool ReadPixelsDepthFloatViaUnsignedInt(GLint x, GLint y, GLsizei width, GLsizei height, void* pixels) { + // GL_DEPTH_COMPONENT readback into the client's layout, honouring the PACK pixel-store + // parameters. GL 4.6 core 18.2.8: the normalized depth is written as-is for GL_FLOAT and + // scaled into the full range of whichever integer width the client asked for otherwise. + static Bool ReadPixelsDepthComponent(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, + void* pixels) { + SizeT dstPixelBytes = 0; + switch (type) { + case GL_UNSIGNED_BYTE: dstPixelBytes = sizeof(Uint8); break; + case GL_UNSIGNED_SHORT: dstPixelBytes = sizeof(Uint16); break; + case GL_UNSIGNED_INT: dstPixelBytes = sizeof(Uint32); break; + case GL_FLOAT: dstPixelBytes = sizeof(GLfloat); break; + default: return false; + } if (width <= 0 || height <= 0) { return true; } - Vector raw; - if (!ReadDepthValuesNative(x, y, width, height, raw)) { - return true; + Vector depth; + if (!ReadDepthValuesNative(x, y, width, height, depth)) { + return true; // already reported; nothing was written, as before } - const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstPixelBytes = sizeof(Float); - const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + - static_cast(width) * dstPixelBytes; - // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - const SizeT pboOffset = reinterpret_cast(pixels); - if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: depth GL_FLOAT fallback PBO is too small"); - return true; - } - Vector rowBuf(static_cast(width)); - for (GLsizei row = 0; row < height; ++row) { - const Float* srcRow = raw.data() + static_cast(row) * static_cast(width); - for (GLsizei col = 0; col < width; ++col) { - rowBuf[col] = srcRow[col]; - } - const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; - if (pixelPackBufferObject) { - pixelPackBufferObject->WritebackFromBackend( - {rowBuf.data(), static_cast(width) * sizeof(Float)}, pboOffset + rowOffset); - } else if (pixels != nullptr) { - Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), - static_cast(width) * sizeof(Float)); - } - } - if (pixelPackBufferObject) { - // WritebackFromBackend bumps change serials with no backend op; re-open - // the buffer draw-clean memos (once for the whole row loop). - BufferImpl::BumpBufferMutationEpoch(); - } + StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "depth", + [&](GLsizei row, Uint8* dst) { + const Float* srcRow = + depth.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + switch (type) { + case GL_UNSIGNED_BYTE: + dst[col] = static_cast( + NormalizedDepthToUnsigned(srcRow[col], 255.0)); + break; + case GL_UNSIGNED_SHORT: + reinterpret_cast(dst)[col] = static_cast( + NormalizedDepthToUnsigned(srcRow[col], 65535.0)); + break; + case GL_UNSIGNED_INT: + reinterpret_cast(dst)[col] = + NormalizedDepthToUnsigned(srcRow[col], 4294967295.0); + break; + default: + reinterpret_cast(dst)[col] = srcRow[col]; + break; + } + } + }); return true; } @@ -5846,39 +6469,50 @@ namespace MobileGL::MG_Backend::DirectGLES { outStencil.assign(static_cast(width) * static_cast(height), 0); 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, outStencil.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - return true; - } + GLenum packedError = GL_INVALID_OPERATION; + if (!MG_Config::Features.EsprytForceDepthStencilReadbackEmulation) { + // Drain first: see ReadDepthValuesNative. + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, outStencil.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + return true; + } - Vector packed(outStencil.size(), 0); - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { - for (SizeT i = 0; i < outStencil.size(); ++i) { - outStencil[i] = static_cast(packed[i] & 0xFFu); + Vector packed(outStencil.size(), 0); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { + for (SizeT i = 0; i < outStencil.size(); ++i) { + outStencil[i] = static_cast(packed[i] & 0xFFu); + } + return true; + } + + // A DEPTH32F_STENCIL8 attachment rejects the 24_8 type: its packed layout is a + // float depth followed by a padded stencil byte, eight bytes per pixel with the + // index at offset 4. + Vector packed32f(outStencil.size() * 8u, 0); + ClearGLErrors(); + g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, + packed32f.data()); + packedError = g_GLESFuncs.glGetError(); + if (packedError == GL_NO_ERROR) { + for (SizeT i = 0; i < outStencil.size(); ++i) { + outStencil[i] = packed32f[i * 8u + 4u]; + } + return true; } - return true; } - // A DEPTH32F_STENCIL8 attachment rejects the 24_8 type: its packed layout is a float depth - // followed by a padded stencil byte, eight bytes per pixel with the index at offset 4. - Vector packed32f(outStencil.size() * 8u, 0); - ClearGLErrors(); - g_GLESFuncs.glReadPixels(x, y, width, height, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, - packed32f.data()); - const GLenum packedError = g_GLESFuncs.glGetError(); - if (packedError != GL_NO_ERROR) { - MGLOG_E("ReadPixels: no stencil readback path is available: %s", - MG_Util::ConvertGLEnumToString(packedError).c_str()); - return false; + // No native spelling worked, which is the ordinary case on ES: sample the stencil + // half of the attachment instead. + if (DepthStencilSamplingReadImpl::Read(x, y, width, height, /*outDepth=*/nullptr, &outStencil)) { + return true; } - for (SizeT i = 0; i < outStencil.size(); ++i) { - outStencil[i] = packed32f[i * 8u + 4u]; - } - return true; + MGLOG_E("ReadPixels: no stencil readback path is available: native reads failed with %s and the " + "sampling emulation could not service the source", + MG_Util::ConvertGLEnumToString(packedError).c_str()); + return false; } // GL_STENCIL_INDEX readback into the client's integer layout, honouring the PACK @@ -5911,54 +6545,75 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - 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 dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT packedSize = dstOffset + static_cast(height - 1) * dstRowStride + - static_cast(width) * dstPixelBytes; - // Only actual pixel rows are written so PACK skip/row-length gap regions stay untouched. - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - const SizeT pboOffset = reinterpret_cast(pixels); - if (pixelPackBufferObject && pboOffset + packedSize > pixelPackBufferObject->GetSize()) { - MGLOG_E("ReadPixels: stencil readback PBO is too small"); + StoreReadbackRowsToClient(width, height, dstPixelBytes, pixels, "stencil", + [&](GLsizei row, Uint8* dst) { + const Uint8* srcRow = + raw.data() + static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + switch (type) { + case GL_UNSIGNED_BYTE: + case GL_BYTE: + dst[static_cast(col)] = srcRow[col]; + break; + case GL_UNSIGNED_SHORT: + case GL_SHORT: + reinterpret_cast(dst)[col] = srcRow[col]; + break; + case GL_FLOAT: + reinterpret_cast(dst)[col] = static_cast(srcRow[col]); + break; + default: + reinterpret_cast(dst)[col] = srcRow[col]; + break; + } + } + }); + return true; + } + + // GL_DEPTH_STENCIL readback: the two aspects are fetched separately and woven into the + // packed layout the client asked for (GL 4.6 core table 8.6). This is what + // KHR-GL3x.packed_depth_stencil.verify_read_pixels / verify_get_tex_image / + // verify_copy_tex_image read their gradients with. + static Bool ReadPixelsDepthStencilPacked(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type, + void* pixels) { + SizeT dstPixelBytes = 0; + switch (type) { + case GL_UNSIGNED_INT_24_8: dstPixelBytes = sizeof(Uint32); break; + case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: dstPixelBytes = sizeof(Float) + sizeof(Uint32); break; + default: return false; + } + if (width <= 0 || height <= 0) { return true; } - const SizeT rowBytes = static_cast(width) * dstPixelBytes; - Vector rowBuf(rowBytes); - for (GLsizei row = 0; row < height; ++row) { - const Uint8* srcRow = raw.data() + static_cast(row) * static_cast(width); - for (GLsizei col = 0; col < width; ++col) { - switch (type) { - case GL_UNSIGNED_BYTE: - case GL_BYTE: - rowBuf[static_cast(col)] = srcRow[col]; - break; - case GL_UNSIGNED_SHORT: - case GL_SHORT: - reinterpret_cast(rowBuf.data())[col] = srcRow[col]; - break; - case GL_FLOAT: - reinterpret_cast(rowBuf.data())[col] = static_cast(srcRow[col]); - break; - default: - reinterpret_cast(rowBuf.data())[col] = srcRow[col]; - break; + + Vector depth; + Vector stencil; + if (!ReadDepthValuesNative(x, y, width, height, depth)) { + return true; + } + if (!ReadStencilBytesNative(x, y, width, height, stencil)) { + return true; + } + + StoreReadbackRowsToClient( + width, height, dstPixelBytes, pixels, "packed depth/stencil", [&](GLsizei row, Uint8* dst) { + const SizeT base = static_cast(row) * static_cast(width); + for (GLsizei col = 0; col < width; ++col) { + const Uint32 stencilIndex = stencil[base + static_cast(col)]; + const Float depthValue = depth[base + static_cast(col)]; + if (type == GL_UNSIGNED_INT_24_8) { + reinterpret_cast(dst)[col] = + (NormalizedDepthToUnsigned(depthValue, 16777215.0) << 8) | stencilIndex; + } else { + // Depth float first, then a word whose low octet is the index and + // whose top 24 bits are unused. + Uint8* pixel = dst + static_cast(col) * (sizeof(Float) + sizeof(Uint32)); + Memcpy(pixel, &depthValue, sizeof(depthValue)); + Memcpy(pixel + sizeof(Float), &stencilIndex, sizeof(stencilIndex)); + } } - } - const SizeT rowOffset = dstOffset + static_cast(row) * dstRowStride; - if (pixelPackBufferObject) { - pixelPackBufferObject->WritebackFromBackend({rowBuf.data(), rowBytes}, pboOffset + rowOffset); - } else if (pixels != nullptr) { - Memcpy(static_cast(pixels) + rowOffset, rowBuf.data(), rowBytes); - } - } - if (pixelPackBufferObject) { - // Serial bumps with no backend op; re-open the buffer draw-clean memos. - BufferImpl::BumpBufferMutationEpoch(); - } + }); return true; } @@ -6386,6 +7041,25 @@ namespace MobileGL::MG_Backend::DirectGLES { type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; } + // The (format, type) pairs the depth/stencil helpers below can service. They are not + // covered by the colour tables above - GetReadbackChannelMapping has no entry for any + // depth or stencil format, so without this gate a read the helpers CAN serve (a + // GL_UNSIGNED_SHORT depth, a GL_SHORT stencil) is turned away before it reaches them. + static Bool IsSupportedDepthStencilReadPixelsPair(GLenum format, GLenum type) { + switch (format) { + case GL_DEPTH_COMPONENT: + return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_SHORT || type == GL_UNSIGNED_INT || + type == GL_FLOAT; + case GL_STENCIL_INDEX: + return type == GL_UNSIGNED_BYTE || type == GL_BYTE || type == GL_UNSIGNED_SHORT || type == GL_SHORT || + type == GL_UNSIGNED_INT || type == GL_INT || type == GL_FLOAT; + case GL_DEPTH_STENCIL: + return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + default: + return false; + } + } + void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { MGLOG_D("ReadPixels: x=%d y=%d w=%d h=%d format=%s type=%s pixels=%p", x, y, width, height, MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str(), pixels); @@ -6393,7 +7067,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // Combinations the ES driver has always handled directly keep the native path; other color layouts go // through the wide-format conversion path. Anything still uncovered degrades to a logged no-op instead // of killing the process; spec-invalid combinations are already rejected with GL errors at the state layer. - const Bool useNativeReadback = IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type); + const Bool useNativeReadback = (IsLegacyNativeReadPixelsFormat(format) && IsLegacyNativeReadPixelsType(type)) || + IsSupportedDepthStencilReadPixelsPair(format, type); ReadbackChannelMapping conversionMapping{}; const Bool convertible = GetReadbackChannelMapping(format, conversionMapping) && GetReadbackDstPixelSize(conversionMapping, type) != 0; @@ -6448,18 +7123,22 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Util::ConvertGLEnumToString(format).c_str(), MG_Util::ConvertGLEnumToString(type).c_str()); return; } - if (format == GL_DEPTH_COMPONENT && type == GL_FLOAT && - ReadPixelsDepthFloatViaUnsignedInt(x, y, width, height, pixels)) { - MGLOG_D("ReadPixels: finished via depth GL_FLOAT fallback"); + // Every depth and stencil read goes through the helpers, not just the widening ones: + // ES has no guaranteed readback for either aspect, so even a byte-for-byte case + // needs the fallback chain (the other native spelling, then the sampling emulation) + // on a driver without GL_NV_read_depth / GL_NV_read_stencil. + if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(x, y, width, height, type, pixels)) { + MGLOG_D("ReadPixels: finished via depth readback helper"); return; } - // Every stencil read goes through the helper, not just the widening ones: ES has no - // guaranteed GL_STENCIL_INDEX readback, so even the byte-for-byte case needs the - // combined GL_DEPTH_STENCIL fallback when the driver lacks GL_NV_read_stencil. if (format == GL_STENCIL_INDEX && ReadPixelsStencilViaNative(x, y, width, height, type, pixels)) { MGLOG_D("ReadPixels: finished via stencil readback helper"); return; } + if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(x, y, width, height, type, pixels)) { + MGLOG_D("ReadPixels: finished via packed depth/stencil readback helper"); + return; + } // 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 @@ -6542,8 +7221,8 @@ namespace MobileGL::MG_Backend::DirectGLES { if (format == GL_RGBA_INTEGER) { return type == GL_INT || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV; } - if (format == GL_DEPTH_STENCIL) { - return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV; + if (format == GL_DEPTH_STENCIL || format == GL_DEPTH_COMPONENT) { + return IsSupportedDepthStencilReadPixelsPair(format, type); } return false; } @@ -6616,10 +7295,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // GL_DEPTH_STENCIL can't be attached as a color attachment (glCheckFramebufferStatus // would report it incomplete); it has its own combined depth+stencil attachment point. // glReadBuffer only selects among color attachments, so it does not apply here. - if (format == GL_DEPTH_STENCIL) { + if (format == GL_DEPTH_STENCIL || format == GL_DEPTH_COMPONENT) { ScratchFBOImpl::EnsureDepthAttachment2D( tempFB, GL_READ_FRAMEBUFFER, backendTexId, - backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, /*withStencil=*/true); + backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, + /*withStencil=*/format == GL_DEPTH_STENCIL); } else 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). @@ -6629,7 +7309,7 @@ namespace MobileGL::MG_Backend::DirectGLES { tempFB, GL_READ_FRAMEBUFFER, backendTexId, backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level); } - if (format != GL_DEPTH_STENCIL) { + if (format != GL_DEPTH_STENCIL && format != GL_DEPTH_COMPONENT) { MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)"); ScratchFBOImpl::EnsureReadBuffer(tempFB, GL_COLOR_ATTACHMENT0); } @@ -6777,6 +7457,20 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + // The level is attached to the scratch READ framebuffer above, so the depth and + // stencil aspects are read with exactly the same helpers glReadPixels uses - native + // where the driver has it, shader sampling where it does not. ES accepts neither + // spelling natively, which is why glGetTexImage(GL_DEPTH_STENCIL) used to leave + // packed_depth_stencil.verify_get_tex_image reading its own zero-filled buffer. + if (format == GL_DEPTH_COMPONENT && ReadPixelsDepthComponent(0, 0, size.x(), size.y(), type, pixels)) { + MGLOG_D("GetTexImage: finished via depth readback helper"); + return; + } + if (format == GL_DEPTH_STENCIL && ReadPixelsDepthStencilPacked(0, 0, size.x(), size.y(), type, pixels)) { + MGLOG_D("GetTexImage: finished via packed depth/stencil readback helper"); + return; + } + // 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 = diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 6ca4b79a..f0fe055b 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -60,6 +60,7 @@ add_executable(MobileGLIntegrationTest Scenarios/FragCoordOriginScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp + Scenarios/DepthStencilReadbackMatrixScenario.cpp Scenarios/SsboArrayLengthScenario.cpp Scenarios/DoublePrecisionScenario.cpp Scenarios/UniformInitializerScenario.cpp @@ -240,6 +241,8 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV}) mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV}) # TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it. set(MGL_ITEST_TIMEOUT 120) @@ -287,3 +290,21 @@ gtest_discover_tests(MobileGLIntegrationTest TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}" ) + +# A fourth registration, of the depth/stencil readback scenarios, with the ES +# shader-sampling emulation forced on. Not paranoia - without it these scenarios are +# UNFALSIFIABLE on the machines this suite runs on: OpenGL ES has no depth or stencil +# readback in core, but Mesa accepts the reads anyway, so on llvmpipe every one of them +# goes green through a native path that the Adreno device does not have. Deleting the +# entire emulation left all of them passing. With the flag the native spellings are off +# the table and only the path the device actually takes remains. DirectGLES only - the +# emulation is DirectGLES's. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.ForcedDepthStencilEmulation." + TEST_FILTER "DepthStencilReadback*Scenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}" +) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp new file mode 100644 index 00000000..8bcc1a18 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp @@ -0,0 +1,784 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackMatrixScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - THE DEPTH/STENCIL READBACK MATRIX: every verb, every source kind. +// +// DepthStencilReadbackScenario pins the default framebuffer. This file pins the rest of +// the surface a depth/stencil read has to cover, because the three verbs and the four +// source kinds do NOT share a code path by accident - they share one on purpose, and a +// change that quietly serves only one of them is exactly what these assertions catch: +// +// verbs glReadPixels(GL_DEPTH_COMPONENT | GL_STENCIL_INDEX | GL_DEPTH_STENCIL), +// glGetTexImage(GL_DEPTH_STENCIL), glCopyTexImage2D followed by a read +// source kinds depth(-stencil) TEXTURE, RENDERBUFFER (not samplable at all), +// MULTISAMPLE renderbuffer (needs a resolve first), default framebuffer +// formats DEPTH24_STENCIL8, DEPTH32F_STENCIL8, DEPTH_COMPONENT16/24/32F, +// STENCIL_INDEX8 +// client types GL_FLOAT / GL_UNSIGNED_INT / GL_UNSIGNED_SHORT depth, GL_INT / +// GL_UNSIGNED_BYTE stencil, both packed GL_DEPTH_STENCIL layouts +// +// On DirectGLES none of this exists natively - ES has no depth or stencil readback in +// core - so every assertion here is really an assertion about the shader-sampling +// emulation. The catch is that some ES drivers accept the reads anyway (Mesa does, +// Adreno does not), which would make the emulation dead code on the very stack the +// headless suite runs on. That is what the second ctest registration is for: the same +// scenarios run again with MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1, which takes the +// native spellings off the table and leaves only the path the device actually uses. +// +// Every destination is poisoned with a value the correct answer cannot be, so "the +// backend wrote nothing" fails loudly instead of passing on a coincidence - a test that +// only checked "no GL error" would pass against a readback that never touched the buffer, +// which is precisely how this whole cluster hid for so long. + +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr float kDepthPoison = 0.2f; + constexpr int kStencilPoison = 50; + constexpr int kWidth = 64; + constexpr int kHeight = 48; + + // A depth-stencil pair no clear in these tests produces, packed both ways. + constexpr unsigned int kPacked24_8Poison = 0xAAAAAA33u; + + struct D32fS8 { + float depth; + unsigned int stencil; + }; + + // Everything a source needs to be read: the framebuffer to bind, plus the objects + // to delete afterwards. + struct DepthSource { + GLuint fbo = 0; + GLuint colorTexture = 0; + GLuint depthTexture = 0; + GLuint depthRenderbuffer = 0; + GLuint colorRenderbuffer = 0; + }; + + void DestroySource(DepthSource& source) { + if (source.fbo != 0) glDeleteFramebuffers(1, &source.fbo); + if (source.colorTexture != 0) glDeleteTextures(1, &source.colorTexture); + if (source.depthTexture != 0) glDeleteTextures(1, &source.depthTexture); + if (source.depthRenderbuffer != 0) glDeleteRenderbuffers(1, &source.depthRenderbuffer); + if (source.colorRenderbuffer != 0) glDeleteRenderbuffers(1, &source.colorRenderbuffer); + source = DepthSource{}; + } + + GLenum AttachmentPointFor(GLenum internalFormat) { + switch (internalFormat) { + case GL_DEPTH24_STENCIL8: + case GL_DEPTH32F_STENCIL8: return GL_DEPTH_STENCIL_ATTACHMENT; + case GL_STENCIL_INDEX8: return GL_STENCIL_ATTACHMENT; + default: return GL_DEPTH_ATTACHMENT; + } + } + + bool FormatHasDepth(GLenum internalFormat) { return internalFormat != GL_STENCIL_INDEX8; } + bool FormatHasStencil(GLenum internalFormat) { + return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 || + internalFormat == GL_STENCIL_INDEX8; + } + + // A framebuffer whose depth/stencil lives in a TEXTURE. The colour attachment is + // there so a stencil-only or depth-only framebuffer still has something to size it. + DepthSource MakeTextureSource(GLenum internalFormat) { + DepthSource source; + glGenFramebuffers(1, &source.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, source.fbo); + glGenTextures(1, &source.colorTexture); + glBindTexture(GL_TEXTURE_2D, source.colorTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source.colorTexture, 0); + glGenTextures(1, &source.depthTexture); + glBindTexture(GL_TEXTURE_2D, source.depthTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kWidth, kHeight); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_TEXTURE_2D, + source.depthTexture, 0); + return source; + } + + // The same, with the depth/stencil in a RENDERBUFFER - which cannot be sampled at + // all, so the readback has no choice but to copy it somewhere samplable first. + // `samples` > 0 makes it multisample, which additionally needs a resolve. + DepthSource MakeRenderbufferSource(GLenum internalFormat, int samples) { + DepthSource source; + glGenFramebuffers(1, &source.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, source.fbo); + glGenRenderbuffers(1, &source.colorRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer); + if (samples > 0) { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kWidth, kHeight); + } else { + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight); + } + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer); + glGenRenderbuffers(1, &source.depthRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer); + if (samples > 0) { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalFormat, kWidth, kHeight); + } else { + glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kWidth, kHeight); + } + glFramebufferRenderbuffer(GL_FRAMEBUFFER, AttachmentPointFor(internalFormat), GL_RENDERBUFFER, + source.depthRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + return source; + } + + // Clears the bound framebuffer's depth and stencil to known values, with the masks + // and the scissor explicitly out of the way (a leaked scissor from an earlier + // scenario would clip the clear and every assertion after it). + void ClearDepthStencil(GLenum internalFormat, float depth, int stencil) { + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, kWidth, kHeight); + GLbitfield mask = 0; + if (FormatHasDepth(internalFormat)) { + glDepthMask(GL_TRUE); + glClearDepth(depth); + mask |= GL_DEPTH_BUFFER_BIT; + } + if (FormatHasStencil(internalFormat)) { + glStencilMask(0xFFu); + glClearStencil(stencil); + mask |= GL_STENCIL_BUFFER_BIT; + } + glClear(mask); + } + + class DepthStencilReadbackMatrixScenario : public ScenarioTest { + protected: + // Not every ES driver can render to every depth format (DEPTH_COMPONENT32F and + // the multisample counts in particular), and an incomplete framebuffer would + // turn a legitimate "this machine cannot host the source" into a spurious + // failure about the readback. + static bool SourceIsUsable() { + return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GLenum(GL_FRAMEBUFFER_COMPLETE); + } + + static std::vector ReadDepthFloat(int x, int y, int width, int height) { + std::vector depth(static_cast(width) * height, kDepthPoison); + glReadPixels(x, y, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data()); + return depth; + } + + static std::vector ReadStencilInt(int x, int y, int width, int height) { + std::vector stencil(static_cast(width) * height, kStencilPoison); + glReadPixels(x, y, width, height, GL_STENCIL_INDEX, GL_INT, stencil.data()); + return stencil; + } + + // "every value in the region is `expected`" rather than "the middle pixel is": + // a staging blit that lands the wrong rectangle, or a conversion pass with a + // half-texel offset, still gets the centre right. + static void ExpectAllDepth(const std::vector& values, float expected, const char* what) { + size_t bad = 0; + float worst = expected; + for (float value : values) { + if (std::fabs(value - expected) > 1.0f / 4096.0f) { + if (bad == 0) worst = value; + ++bad; + } + } + EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size() + << " depth values differ from " << expected << "; first bad value " << worst + << (std::fabs(worst - kDepthPoison) < 1e-6f + ? " - which is the poison value, so nothing was written at all" + : ""); + } + + static void ExpectAllStencil(const std::vector& values, int expected, const char* what) { + size_t bad = 0; + int worst = expected; + for (int value : values) { + if (value != expected) { + if (bad == 0) worst = value; + ++bad; + } + } + EXPECT_EQ(bad, 0u) << what << ": " << bad << " of " << values.size() + << " stencil values differ from " << expected << "; first bad value " << worst + << (worst == kStencilPoison + ? " - which is the poison value, so nothing was written at all" + : ""); + } + }; + + // ---- glReadPixels across the source kinds ----------------------------------- + + struct SourceCase { + const char* name; + GLenum internalFormat; + int samples; + bool renderbuffer; + }; + + const SourceCase kSourceCases[] = { + {"texture depth24_stencil8", GL_DEPTH24_STENCIL8, 0, false}, + {"texture depth32f_stencil8", GL_DEPTH32F_STENCIL8, 0, false}, + {"texture depth_component16", GL_DEPTH_COMPONENT16, 0, false}, + {"texture depth_component24", GL_DEPTH_COMPONENT24, 0, false}, + {"texture depth_component32f", GL_DEPTH_COMPONENT32F, 0, false}, + {"renderbuffer depth24_stencil8", GL_DEPTH24_STENCIL8, 0, true}, + {"renderbuffer depth_component24", GL_DEPTH_COMPONENT24, 0, true}, + {"renderbuffer stencil_index8", GL_STENCIL_INDEX8, 0, true}, + }; + + } // namespace + + TEST_F(DepthStencilReadbackMatrixScenario, EverySourceKindReadsItsClearBack) { + if (!Ready()) return; + int exercised = 0; + for (const SourceCase& testCase : kSourceCases) { + SCOPED_TRACE(testCase.name); + DepthSource source = testCase.renderbuffer + ? MakeRenderbufferSource(testCase.internalFormat, testCase.samples) + : MakeTextureSource(testCase.internalFormat); + if (!SourceIsUsable()) { + DestroySource(source); + continue; + } + FirstGLError(); // the storage calls above may have probed an unsupported combination + ClearDepthStencil(testCase.internalFormat, 0.625f, 9); + EXPECT_EQ(FirstGLError(), 0u) << "clearing the source"; + + if (FormatHasDepth(testCase.internalFormat)) { + const std::vector depth = ReadDepthFloat(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_FLOAT)"; + ExpectAllDepth(depth, 0.625f, testCase.name); + } + if (FormatHasStencil(testCase.internalFormat)) { + const std::vector stencil = ReadStencilInt(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)"; + ExpectAllStencil(stencil, 9, testCase.name); + } + ++exercised; + DestroySource(source); + } + // A machine that hosted none of the sources would report a vacuous pass. + EXPECT_GE(exercised, 4) << "too few depth/stencil source kinds were usable to call this a matrix"; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // Depth and stencil in two SEPARATE objects, with two different formats, on the same + // framebuffer. Legal GL, and the shape KHR-GL3x.framebuffer_blit builds when its depth + // config and its stencil config are configured independently - so a readback that + // describes "the" depth/stencil source as one thing serves whichever aspect it happened + // to find first and silently abandons the other. Each aspect has to be staged from its + // own attachment, in its own format. + TEST_F(DepthStencilReadbackMatrixScenario, SeparateDepthAndStencilAttachmentsAreBothReadable) { + if (!Ready()) return; + DepthSource source; + glGenFramebuffers(1, &source.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, source.fbo); + glGenRenderbuffers(1, &source.colorRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, source.colorRenderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, source.colorRenderbuffer); + // Depth in a DEPTH_COMPONENT24 renderbuffer... + glGenRenderbuffers(1, &source.depthRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, source.depthRenderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, kWidth, kHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, source.depthRenderbuffer); + // ...and stencil in a STENCIL_INDEX8 one of its own. + GLuint stencilRenderbuffer = 0; + glGenRenderbuffers(1, &stencilRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, stencilRenderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, kWidth, kHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + if (!SourceIsUsable()) { + // Separate depth and stencil images are legal GL but many stacks answer + // GL_FRAMEBUFFER_UNSUPPORTED for them; say which, so a skip here is a fact about + // the driver rather than an unexplained hole in the matrix. + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glDeleteRenderbuffers(1, &stencilRenderbuffer); + DestroySource(source); + GTEST_SKIP() << "this driver cannot host separate DEPTH_COMPONENT24 and STENCIL_INDEX8 attachments: " + << "glCheckFramebufferStatus = 0x" << std::hex << status; + } + FirstGLError(); + + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, kWidth, kHeight); + glDepthMask(GL_TRUE); + glStencilMask(0xFFu); + glClearDepth(0.3125); + glClearStencil(17); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), 0u); + + const std::vector depth = ReadDepthFloat(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u) << "reading depth from a separately-attached DEPTH_COMPONENT24"; + ExpectAllDepth(depth, 0.3125f, "separate depth attachment"); + + const std::vector stencil = ReadStencilInt(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u) << "reading stencil from a separately-attached STENCIL_INDEX8"; + ExpectAllStencil(stencil, 17, "separate stencil attachment"); + + glDeleteRenderbuffers(1, &stencilRenderbuffer); + DestroySource(source); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // A multisample source is never read directly - glReadPixels on a multisampled + // framebuffer is INVALID_OPERATION in GL as much as in ES, and the state layer says so. + // The way multisample depth reaches a reader is a resolve blit into a single-sampled + // framebuffer, which is then read; that pair is + // KHR-GL3x.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test, and + // the assertion here is that the resolved depth arrives intact rather than as the + // destination's own clear value. + TEST_F(DepthStencilReadbackMatrixScenario, AResolvedMultisampleDepthReadsBackFromTheDestination) { + if (!Ready()) return; + DepthSource multisampled = MakeRenderbufferSource(GL_DEPTH24_STENCIL8, 4); + if (!SourceIsUsable()) { + DestroySource(multisampled); + GTEST_SKIP() << "this driver cannot host a 4x multisample DEPTH24_STENCIL8 renderbuffer"; + } + FirstGLError(); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.875f, 0); + ASSERT_EQ(FirstGLError(), 0u); + + // The destination starts at a depth the resolve must overwrite everywhere. + DepthSource resolved = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.125f, 0); + ASSERT_EQ(FirstGLError(), 0u); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampled.fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo); + glDisable(GL_SCISSOR_TEST); + glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + EXPECT_EQ(FirstGLError(), 0u) << "resolving a multisample depth buffer into a single-sampled one"; + + glBindFramebuffer(GL_FRAMEBUFFER, resolved.fbo); + const std::vector depth = ReadDepthFloat(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u); + ExpectAllDepth(depth, 0.875f, "resolved multisample depth"); + + DestroySource(resolved); + DestroySource(multisampled); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // A read whose rectangle is NOT the whole attachment. The staging copy has to carry + // the requested rect (not the origin) and hand back its rows bottom-up, which a + // full-extent uniform read is a fixed point of and therefore cannot see. + TEST_F(DepthStencilReadbackMatrixScenario, ASubRectangleReadsTheRightBandInTheRightOrder) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + + // Bottom half 0.25, top half 0.75, and the stencil banded the other way round so a + // mix-up between the two aspects cannot pass either. + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, kWidth, kHeight); + glDepthMask(GL_TRUE); + glStencilMask(0xFFu); + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, kWidth, kHeight / 2); + glClearDepth(0.25); + glClearStencil(11); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glScissor(0, kHeight / 2, kWidth, kHeight - kHeight / 2); + glClearDepth(0.75); + glClearStencil(22); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + ASSERT_EQ(FirstGLError(), 0u); + + // A rect wholly inside the bottom band, offset from the origin in both axes. + const int rectWidth = 8; + const int rectHeight = 4; + const std::vector bottom = ReadDepthFloat(16, 4, rectWidth, rectHeight); + EXPECT_EQ(FirstGLError(), 0u); + ExpectAllDepth(bottom, 0.25f, "sub-rect inside the bottom depth band"); + const std::vector bottomStencil = ReadStencilInt(16, 4, rectWidth, rectHeight); + EXPECT_EQ(FirstGLError(), 0u); + ExpectAllStencil(bottomStencil, 11, "sub-rect inside the bottom stencil band"); + + // And one wholly inside the top band. Reading the mirrored row would answer 0.25. + const std::vector top = ReadDepthFloat(16, kHeight - 4 - rectHeight, rectWidth, rectHeight); + EXPECT_EQ(FirstGLError(), 0u); + ExpectAllDepth(top, 0.75f, "sub-rect inside the top depth band"); + + // A rect that STRADDLES the boundary pins the row order itself: its first rows must + // be the bottom band and its last rows the top one. + const int straddleHeight = 8; + const std::vector straddle = + ReadDepthFloat(16, kHeight / 2 - straddleHeight / 2, rectWidth, straddleHeight); + EXPECT_EQ(FirstGLError(), 0u); + ASSERT_EQ(straddle.size(), static_cast(rectWidth) * straddleHeight); + EXPECT_NEAR(straddle[0], 0.25f, 1.0f / 4096.0f) + << "the first row of the returned rect must be its BOTTOM row (GL order), which is in the 0.25 band"; + EXPECT_NEAR(straddle[straddle.size() - 1], 0.75f, 1.0f / 4096.0f) + << "the last row of the returned rect must be its TOP row, which is in the 0.75 band"; + + DestroySource(source); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // The packed layouts the packed_depth_stencil family reads its gradients with. + TEST_F(DepthStencilReadbackMatrixScenario, PackedDepthStencilReadPixelsCarriesBothAspects) { + if (!Ready()) return; + struct PackedCase { + const char* name; + GLenum internalFormat; + GLenum type; + }; + const PackedCase cases[] = { + {"depth24_stencil8 / GL_UNSIGNED_INT_24_8", GL_DEPTH24_STENCIL8, GL_UNSIGNED_INT_24_8}, + {"depth32f_stencil8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV", GL_DEPTH32F_STENCIL8, + GL_FLOAT_32_UNSIGNED_INT_24_8_REV}, + }; + int exercised = 0; + for (const PackedCase& testCase : cases) { + SCOPED_TRACE(testCase.name); + DepthSource source = MakeTextureSource(testCase.internalFormat); + if (!SourceIsUsable()) { + DestroySource(source); + continue; + } + FirstGLError(); + ClearDepthStencil(testCase.internalFormat, 0.5f, 3); + ASSERT_EQ(FirstGLError(), 0u); + + const size_t pixels = static_cast(kWidth) * kHeight; + if (testCase.type == GL_UNSIGNED_INT_24_8) { + std::vector packed(pixels, kPacked24_8Poison); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data()); + EXPECT_EQ(FirstGLError(), 0u); + size_t bad = 0; + for (unsigned int value : packed) { + const float depth = static_cast(value >> 8) / 16777215.0f; + const int stencil = static_cast(value & 0xFFu); + if (std::fabs(depth - 0.5f) > 0.01f || stencil != 3) ++bad; + } + EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels + << " packed words carry the wrong depth or stencil (first word 0x" << std::hex + << packed[0] << std::dec << ")"; + } else { + std::vector packed(pixels, D32fS8{kDepthPoison, static_cast(kStencilPoison)}); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, testCase.type, packed.data()); + EXPECT_EQ(FirstGLError(), 0u); + size_t bad = 0; + for (const D32fS8& value : packed) { + if (std::fabs(value.depth - 0.5f) > 0.01f || (value.stencil & 0xFFu) != 3u) ++bad; + } + EXPECT_EQ(bad, 0u) << testCase.name << ": " << bad << " of " << pixels + << " packed pairs carry the wrong depth or stencil (first pair depth " + << packed[0].depth << " stencil " << (packed[0].stencil & 0xFFu) << ")"; + } + ++exercised; + DestroySource(source); + } + EXPECT_GE(exercised, 1) << "neither packed depth/stencil format was renderable"; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // glGetTexImage reads a TEXTURE, not the bound framebuffer - a different entry point + // that has to reach the same machinery. This is verify_get_tex_image's shape. + TEST_F(DepthStencilReadbackMatrixScenario, GetTexImageReadsAPackedDepthStencilTexture) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + FirstGLError(); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.375f, 5); + ASSERT_EQ(FirstGLError(), 0u); + + // Read it back through the texture, with the framebuffer that owns it unbound so a + // path that secretly read the framebuffer instead would answer from somewhere else. + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, source.depthTexture); + const size_t pixels = static_cast(kWidth) * kHeight; + std::vector packed(pixels, kPacked24_8Poison); + glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); + EXPECT_EQ(FirstGLError(), 0u); + size_t bad = 0; + for (unsigned int value : packed) { + const float depth = static_cast(value >> 8) / 16777215.0f; + if (std::fabs(depth - 0.375f) > 0.01f || (value & 0xFFu) != 5u) ++bad; + } + EXPECT_EQ(bad, 0u) << bad << " of " << pixels + << " words from glGetTexImage(GL_DEPTH_STENCIL) are wrong (first word 0x" << std::hex + << packed[0] << std::dec << ")"; + + glBindTexture(GL_TEXTURE_2D, 0); + DestroySource(source); + Gl().EndFrame(); + } + + // glCopyTexImage2D out of a depth attachment, then read the copy - verify_copy_tex_image. + TEST_F(DepthStencilReadbackMatrixScenario, CopyTexImageFromADepthAttachmentSurvivesAReadBack) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + FirstGLError(); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.75f, 6); + ASSERT_EQ(FirstGLError(), 0u); + + GLuint copy = 0; + glGenTextures(1, ©); + glBindTexture(GL_TEXTURE_2D, copy); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, kWidth, kHeight, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 0, 0, kWidth, kHeight, 0); + EXPECT_EQ(FirstGLError(), 0u) << "glCopyTexImage2D from a depth/stencil attachment"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + const size_t pixels = static_cast(kWidth) * kHeight; + std::vector packed(pixels, kPacked24_8Poison); + glGetTexImage(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, packed.data()); + EXPECT_EQ(FirstGLError(), 0u); + size_t bad = 0; + for (unsigned int value : packed) { + const float depth = static_cast(value >> 8) / 16777215.0f; + if (std::fabs(depth - 0.75f) > 0.01f) ++bad; + } + EXPECT_EQ(bad, 0u) << bad << " of " << pixels << " copied depth values are wrong (first word 0x" << std::hex + << packed[0] << std::dec << ")"; + + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, ©); + DestroySource(source); + Gl().EndFrame(); + } + + // The integer client widths, which are a separate conversion each. + TEST_F(DepthStencilReadbackMatrixScenario, DepthAndStencilConvertIntoEveryClientWidth) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + FirstGLError(); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 200); + ASSERT_EQ(FirstGLError(), 0u); + + const size_t pixels = static_cast(kWidth) * kHeight; + + std::vector depthUint(pixels, 0xDEADBEEFu); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, depthUint.data()); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_INT)"; + // 0.5 of the full 32-bit range, with room for the source's 24-bit quantisation. + EXPECT_NEAR(static_cast(depthUint[0]) / 4294967295.0, 0.5, 0.01) + << "GL_UNSIGNED_INT depth came back as " << depthUint[0]; + + std::vector depthUshort(pixels, 0xBEEFu); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, depthUshort.data()); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT)"; + EXPECT_NEAR(static_cast(depthUshort[0]) / 65535.0, 0.5, 0.01) + << "GL_UNSIGNED_SHORT depth came back as " << depthUshort[0]; + + // A stencil index is written unconverted into whichever width was asked for, so 200 + // must survive intact in all of them - it is also large enough that a signed byte + // would wrap, which is the point of choosing it. + std::vector stencilByte(pixels, static_cast(kStencilPoison)); + glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilByte.data()); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_UNSIGNED_BYTE)"; + EXPECT_EQ(static_cast(stencilByte[0]), 200); + + std::vector stencilInt(pixels, kStencilPoison); + glReadPixels(0, 0, kWidth, kHeight, GL_STENCIL_INDEX, GL_INT, stencilInt.data()); + EXPECT_EQ(FirstGLError(), 0u) << "glReadPixels(GL_STENCIL_INDEX, GL_INT)"; + EXPECT_EQ(stencilInt[0], 200); + + DestroySource(source); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // The PACK pixel-store parameters apply to a depth read exactly as they do to a colour + // one, and the gap regions they create must be left alone. + TEST_F(DepthStencilReadbackMatrixScenario, DepthReadbackHonoursThePackPixelStoreParameters) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH_COMPONENT24); + ASSERT_TRUE(SourceIsUsable()); + FirstGLError(); + ClearDepthStencil(GL_DEPTH_COMPONENT24, 0.5f, 0); + ASSERT_EQ(FirstGLError(), 0u); + + const int rectWidth = 4; + const int rectHeight = 3; + const int rowLength = 8; + const int skipPixels = 2; + const int skipRows = 1; + constexpr float kGap = -7.0f; + std::vector destination(static_cast(rowLength) * (skipRows + rectHeight) + 16, kGap); + + glPixelStorei(GL_PACK_ROW_LENGTH, rowLength); + glPixelStorei(GL_PACK_SKIP_PIXELS, skipPixels); + glPixelStorei(GL_PACK_SKIP_ROWS, skipRows); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, 0, rectWidth, rectHeight, GL_DEPTH_COMPONENT, GL_FLOAT, destination.data()); + const unsigned int readError = FirstGLError(); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + EXPECT_EQ(readError, 0u); + + size_t written = 0; + size_t gapsTouched = 0; + for (size_t index = 0; index < destination.size(); ++index) { + const long row = static_cast(index) / rowLength - skipRows; + const long column = static_cast(index) % rowLength - skipPixels; + const bool inRect = row >= 0 && row < rectHeight && column >= 0 && column < rectWidth; + if (inRect) { + if (std::fabs(destination[index] - 0.5f) <= 1.0f / 4096.0f) ++written; + } else if (destination[index] != kGap) { + ++gapsTouched; + } + } + EXPECT_EQ(written, static_cast(rectWidth) * rectHeight) + << "only " << written << " of " << (rectWidth * rectHeight) + << " destination pixels landed where GL_PACK_ROW_LENGTH/SKIP_* put them"; + EXPECT_EQ(gapsTouched, 0u) << gapsTouched << " bytes outside the packed rectangle were overwritten"; + + DestroySource(source); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + + // The readback borrows the application's context for a full-screen pass. Everything it + // touches has to come back, or the next draw inherits it - which is how an emulation + // that "works" takes the rest of the renderer down with it. + TEST_F(DepthStencilReadbackMatrixScenario, ReadbackLeavesNoGLStateBehind) { + if (!Ready()) return; + DepthSource source = MakeTextureSource(GL_DEPTH24_STENCIL8); + ASSERT_TRUE(SourceIsUsable()); + FirstGLError(); + ClearDepthStencil(GL_DEPTH24_STENCIL8, 0.5f, 4); + + // A deliberately awkward state: nothing here is what an emulation pass would want, + // so anything it forgets to put back shows up below. + GLuint scratchTexture = 0; + glGenTextures(1, &scratchTexture); + glBindTexture(GL_TEXTURE_2D, scratchTexture); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, scratchTexture); + glEnable(GL_SCISSOR_TEST); + glScissor(3, 5, 7, 11); + glEnable(GL_CULL_FACE); + glEnable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_GEQUAL); + glDepthMask(GL_FALSE); + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_NOTEQUAL, 0x5, 0x0Fu); + glStencilOp(GL_INCR, GL_DECR, GL_INVERT); + glStencilMask(0x3Cu); + glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE); + glViewport(2, 3, 5, 7); + ASSERT_EQ(FirstGLError(), 0u); + + const std::vector depth = ReadDepthFloat(0, 0, kWidth, kHeight); + const std::vector stencil = ReadStencilInt(0, 0, kWidth, kHeight); + EXPECT_EQ(FirstGLError(), 0u); + ExpectAllDepth(depth, 0.5f, "state-preservation case depth"); + ExpectAllStencil(stencil, 4, "state-preservation case stencil"); + + GLint viewport[4] = {0, 0, 0, 0}; + GLint scissorBox[4] = {0, 0, 0, 0}; + GLboolean colorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; + GLint depthFunc = 0; + GLboolean depthMask = GL_TRUE; + GLint stencilFunc = 0, stencilRef = 0, stencilValueMask = 0, stencilWriteMask = 0; + GLint stencilFail = 0, stencilPassDepthFail = 0, stencilPassDepthPass = 0; + GLint activeTexture = 0, boundTexture = 0; + glGetIntegerv(GL_VIEWPORT, viewport); + glGetIntegerv(GL_SCISSOR_BOX, scissorBox); + glGetBooleanv(GL_COLOR_WRITEMASK, colorMask); + glGetIntegerv(GL_DEPTH_FUNC, &depthFunc); + glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask); + glGetIntegerv(GL_STENCIL_FUNC, &stencilFunc); + glGetIntegerv(GL_STENCIL_REF, &stencilRef); + glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask); + glGetIntegerv(GL_STENCIL_WRITEMASK, &stencilWriteMask); + glGetIntegerv(GL_STENCIL_FAIL, &stencilFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &stencilPassDepthFail); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &stencilPassDepthPass); + glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTexture); + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTexture); + + EXPECT_EQ(viewport[0], 2); + EXPECT_EQ(viewport[1], 3); + EXPECT_EQ(viewport[2], 5); + EXPECT_EQ(viewport[3], 7); + EXPECT_EQ(scissorBox[0], 3); + EXPECT_EQ(scissorBox[1], 5); + EXPECT_EQ(scissorBox[2], 7); + EXPECT_EQ(scissorBox[3], 11); + EXPECT_EQ(glIsEnabled(GL_SCISSOR_TEST), GLboolean(GL_TRUE)); + EXPECT_EQ(glIsEnabled(GL_CULL_FACE), GLboolean(GL_TRUE)); + EXPECT_EQ(glIsEnabled(GL_BLEND), GLboolean(GL_TRUE)); + EXPECT_EQ(glIsEnabled(GL_DEPTH_TEST), GLboolean(GL_TRUE)); + EXPECT_EQ(glIsEnabled(GL_STENCIL_TEST), GLboolean(GL_TRUE)); + EXPECT_EQ(colorMask[0], GLboolean(GL_FALSE)); + EXPECT_EQ(colorMask[1], GLboolean(GL_TRUE)); + EXPECT_EQ(colorMask[2], GLboolean(GL_FALSE)); + EXPECT_EQ(colorMask[3], GLboolean(GL_TRUE)); + EXPECT_EQ(depthFunc, GLint(GL_GEQUAL)); + EXPECT_EQ(depthMask, GLboolean(GL_FALSE)); + EXPECT_EQ(stencilFunc, GLint(GL_NOTEQUAL)); + EXPECT_EQ(stencilRef, 0x5); + EXPECT_EQ(stencilValueMask, 0x0F); + EXPECT_EQ(stencilWriteMask, 0x3C); + EXPECT_EQ(stencilFail, GLint(GL_INCR)); + EXPECT_EQ(stencilPassDepthFail, GLint(GL_DECR)); + EXPECT_EQ(stencilPassDepthPass, GLint(GL_INVERT)); + EXPECT_EQ(activeTexture, GLint(GL_TEXTURE3)); + EXPECT_EQ(boundTexture, GLint(scratchTexture)) + << "the readback left a scratch texture on the application's texture unit"; + EXPECT_EQ(FirstGLError(), 0u); + + // Put the awkward state back so the next scenario in this process starts clean. + glDisable(GL_SCISSOR_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFFu); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilMask(0xFFFFFFFFu); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glDeleteTextures(1, &scratchTexture); + DestroySource(source); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, Gl().Width(), Gl().Height()); + Gl().EndFrame(); + } + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp index 6a41efae..8acd520c 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp @@ -58,12 +58,12 @@ namespace MGITest { class DepthStencilReadbackScenario : public ScenarioTest { protected: - // DirectGLES reads depth and stencil back through the ES driver, which has no - // guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and - // absent on both the Adreno device and Mesa's ES). That gap is tracked separately as - // the packed_depth_stencil cluster and needs a shader-sampling emulation, not this - // change; asserting it here would only pin a known-missing feature. - bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; } + // Both backends now answer these reads. DirectGLES has no native ES path for + // either aspect (GL_NV_read_depth / GL_NV_read_stencil are optional and absent on + // both the Adreno device and Mesa's ES), so it stages the attachment into a + // scratch depth texture and samples it into a colour target; the assertions below + // are the same either way, which is the point. + bool BackendReadsDepthStencil() const { return true; } float ReadDepthAt(int x, int y) const { float depth = kDepthPoison; diff --git a/README.md b/README.md index d0e3b08d..9b75e6dd 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ MobileGL supports runtime configuration via environment variables. | `MOBILEGL_MAGMA_FRAMESINFLIGHT` | Set Magma frames in flight. | Integer `1`–`64` | `3` | | `MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER` | Avoid sampler mipmap minification filters. | `0`, `1` | `0` | | `MOBILEGL_COHERENT_AS_FLUSH` | Treat persistent `GL_MAP_FLUSH_EXPLICIT_BIT` maps as coherent (app-compat for engines like Flywheel that never flush them). | `0`, `1` | `0` | +| `MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION` | Always emulate depth/stencil `glReadPixels`/`glGetTexImage` by shader sampling on Espryt, instead of using the driver's own depth/stencil readback where it has one. | `0`, `1` | `0` | | `VK_ICD_FILENAMES` | Select the Vulkan ICD used by the Vulkan loader. | Path to an ICD JSON file | Loader default | ## License