From cf0f77b0671c777efd03b91bd31925146da97804 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 12:13:19 -0400 Subject: [PATCH] [MG_Backend, Test] isolate native mip completeness during GLES texture readback --- .../DirectGLES/WireTextureReadback.inc | 65 ++++++++++++++++++- ...learTexImageUndefinedLevelZeroScenario.cpp | 57 ++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc index 89ab83d7..2c7dcaf5 100644 --- a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc +++ b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc @@ -1,6 +1,60 @@ // Included in DirectGLES.cpp after the native packed-word readback helper. // The server needs owned tight rows for a storage promotion, not client PACK // layout or a fallback into a frontend texture's potentially stale shadow. + class ScopedWireTextureReadLevel { + public: + ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level) + : m_texture(texture), m_target(target) { + switch (target) { + case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break; + case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break; + case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break; + case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break; + case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break; + default: return; + } + GLint previous = 0; + DrainESErrors(); + g_GLESFuncs.glGetIntegerv(m_binding, &previous); + g_GLESFuncs.glBindTexture(m_target, m_texture); + g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel); + g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel); + m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR; + if (m_saved) { + // ES framebuffer completeness can reject a defined level whose + // base level or intervening mip chain is missing/inconsistent. + // Read this existing native image as the sole base level. The + // logical GL parameters and every cached copy remain unchanged. + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level); + m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); + } + + ~ScopedWireTextureReadLevel() { + if (!m_saved) return; + // The packed-word path can use its own texture scratch binding. + // Save the binding at restoration time too; do not assume an active + // texture unit or make the backend's binding cache forget a bind. + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(m_binding, &previous); + g_GLESFuncs.glBindTexture(m_target, m_texture); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel); + g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel); + g_GLESFuncs.glBindTexture(m_target, static_cast(previous)); + } + + Bool Valid() const { return m_valid; } + + private: + GLuint m_texture; + GLenum m_target; + GLenum m_binding = GL_NONE; + GLint m_baseLevel = 0, m_maxLevel = 0; + Bool m_saved = false, m_valid = false; + }; + static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type, Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier, Vector& bytes) { @@ -164,13 +218,15 @@ Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes) { if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false; const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); + const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level); + if (!readLevel.Valid()) return false; if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX) return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes); ReadbackChannelMapping mapping{}; if (!GetReadbackChannelMapping(format, mapping)) return false; const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false; - const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); const auto widening = sourceUsesImageCarrier ? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{}; @@ -208,7 +264,12 @@ else ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level); ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0); - if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false; + const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x", + status, texture, level, nativeTarget); + return false; + } Vector slice; if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type, TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) || diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp index 8c4f2ba2..9cb5782b 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include "../Harness/HeadlessGL.h" @@ -247,4 +248,60 @@ namespace MGITest { Gl().EndFrame(); } + // A native readback may temporarily isolate a mip for ES completeness. The + // following draws observe the actual sampling state, not a frontend getter + // that would hide a leaked native BASE_LEVEL/MAX_LEVEL change. + TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadingMipLevelsPreservesSubsequentSamplingLodRange) { + if (!Ready()) GTEST_SKIP(); + const char* vertex = R"(#version 330 core +void main() { vec2 p[3]=vec2[3](vec2(-1,-1),vec2(3,-1),vec2(-1,3)); +gl_Position=vec4(p[gl_VertexID],0,1); })"; + const char* fragment = R"(#version 330 core +uniform sampler2D source; uniform float lod; out vec4 color; +void main() { color=textureLod(source,vec2(0.5),lod); })"; + std::string error; + const GLuint program = CompileProgram(vertex, fragment, &error); + ASSERT_NE(program, 0u) << error; + struct Objects { + GLuint program, vao = 0, fbo = 0, output = 0; + ~Objects() { + glUseProgram(0); glBindVertexArray(0); glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); glDeleteTextures(1, &output); + glDeleteVertexArrays(1, &vao); glDeleteProgram(program); + } + } objects{program}; + glGenVertexArrays(1, &objects.vao); glBindVertexArray(objects.vao); + glActiveTexture(GL_TEXTURE0); + glGenTextures(1, &objects.output); glBindTexture(GL_TEXTURE_2D, objects.output); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenFramebuffers(1, &objects.fbo); glBindFramebuffer(GL_FRAMEBUFFER, objects.fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, objects.output, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + MakeTextureWithOnlyLevel(0); + const std::vector upper(4 * 4, kClearValue); + glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, upper.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); glDisable(GL_SCISSOR_TEST); glDisable(GL_CULL_FACE); + glDisable(GL_STENCIL_TEST); glDisable(GL_RASTERIZER_DISCARD); glDisable(GL_FRAMEBUFFER_SRGB); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glUseProgram(program); + const GLint lod = glGetUniformLocation(program, "lod"); + ASSERT_GE(lod, 0); + std::vector readUpper(4 * 4); + glGetTexImage(GL_TEXTURE_2D, 1, GL_RGBA, GL_UNSIGNED_BYTE, readUpper.data()); + ExpectAllTexels("upper level readback", readUpper, kClearValue); + glViewport(0, 0, 4, 4); glUniform1f(lod, 0); glDrawArrays(GL_TRIANGLES, 0, 3); + ExpectAllTexels("base level readback", ReadLevel(0), kInitialValue); + glViewport(4, 0, 4, 4); glUniform1f(lod, 1); glDrawArrays(GL_TRIANGLES, 0, 3); + Texel8 baseSample{}, upperSample{}; + glReadPixels(2, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &baseSample); + glReadPixels(6, 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &upperSample); + EXPECT_EQ(baseSample, kInitialValue) << "readback must restore native BASE_LEVEL"; + EXPECT_EQ(upperSample, kClearValue) << "readback must restore native MAX_LEVEL"; + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + } + } // namespace MGITest