From 394d1ce748529dd80a7f26630b58f654c1519d13 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Wed, 5 Aug 2026 05:50:32 -0400 Subject: [PATCH] [Feat] (MG_Impl, MG_Util): copy into 1D and 3D textures, and accept the BPTC and ETC2 enums Two unrelated texture gaps. glCopyTextureSubImage1D and 3D validated their arguments and then did nothing: CopyTexSubImage1D_State and CopyTexSubImage3D_State were empty TODOs and no backend exposes anything but a 2D blit. But a texture's contents live in its CPU storage - the backends sync from it - so the copy does not need a blit at all. CopyReadFramebufferIntoMipmapRegion reads the region out of the read framebuffer through the existing ReadPixels path, in the destination's own canonical client layout so the bytes need no second conversion, and writes them straight into the level. GL 4.6 core 8.6 says the copy ignores pixel-store state and any bound pack buffer, which the borrowed readback does not, so both are neutralised for the duration and restored after. A cube map destination addresses its faces as separate upload targets, so its zoffset picks the target rather than a slice. ConvertGLEnumToTextureInternalFormat had arms for the six generic compressed formats and the four RGTC ones, all resolving to uncompressed storage, but none for BPTC or ETC2/EAC - so glTexImage2D with one of those fourteen enums answered INVALID_ENUM, which was never a legal reply for formats core GL has required since 4.2 and 4.3. They follow the same deviation for the same reason: nothing in this stack can compress them, and uncompressed storage is the trade the RGTC formats already take. Takes textures_compressed_subimage from failing to passing on both backends and textures_copy on Espryt. textures_copy still fails on Magma, where the readback of a layered attachment does not yet resolve the attached layer. --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 111 ++++++++++++++++-- .../GLToMG/TextureEnumConverter.cpp | 29 +++++ 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 9f24e5a5..96e260ae 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -725,6 +725,96 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->MarkStorageDirty(uploadTarget, static_cast(level), true); return true; } + // GL 4.6 core 8.6: CopyTexSubImage* is not affected by pixel-store state or by a bound + // pack buffer, but the backend readback this borrows honours both. Neutralise them for the + // duration of the read and put them back afterwards. + class ScopedNeutralPackState { + public: + ScopedNeutralPackState() { + for (SizeT i = 0; i < kParams.size(); ++i) { + m_saved[i] = MG_State::pGLContext->GetPixelStoreParam(kParams[i]); + MG_State::pGLContext->SetPixelStoreParam(kParams[i], i == 0 ? 1 : 0); + } + auto& slot = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack); + m_savedPackBuffer = slot.GetBoundObject(); + slot.Bind(nullptr); + } + ~ScopedNeutralPackState() { + for (SizeT i = 0; i < kParams.size(); ++i) { + MG_State::pGLContext->SetPixelStoreParam(kParams[i], m_saved[i]); + } + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).Bind(m_savedPackBuffer); + } + + private: + // PackAlignment must be first: it is the one that resets to 1 rather than 0. + static constexpr Array kParams{ + PixelStoreParam::PackAlignment, PixelStoreParam::PackRowLength, + PixelStoreParam::PackImageHeight, PixelStoreParam::PackSkipRows, + PixelStoreParam::PackSkipPixels, PixelStoreParam::PackSkipImages, + PixelStoreParam::PackSwapBytes, PixelStoreParam::PackLSBFirst}; + Array m_saved{}; + SharedPtr m_savedPackBuffer; + }; + + // The copy half of glCopyTexSubImage*: read the region out of the read framebuffer and write + // it into the destination level's CPU storage. Done in the frontend because that storage is + // where a texture's contents actually live - the backends sync from it - so this needs no + // 1D or 3D blit, which neither backend has. + Bool CopyReadFramebufferIntoMipmapRegion(const SharedPtr& textureObject, + TextureUploadTarget uploadTarget, GLint level, GLint xoffset, + GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, + GLsizei height, const char* caller) { + if (width <= 0 || height <= 0) return true; + auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get()); + if (!mipmapTexture) return false; + + const auto texelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast(level)); + const SizeT texelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * + static_cast(texelSize.z()); + const SizeT byteSize = mipmapTexture->GetMipmapByteSize(uploadTarget, static_cast(level)); + if (texelCount == 0 || byteSize == 0 || byteSize % texelCount != 0) return false; + const SizeT bytesPerTexel = byteSize / texelCount; + + // Read in the destination's own canonical client layout, so the bytes land in storage + // without a second conversion. + const GLenum glInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()); + GLenum realInternalFormat = glInternalFormat; + GLenum format = GL_RGBA; + GLenum type = GL_UNSIGNED_BYTE; + MG_Util::TextureFormatProcessor::NormalizePixelFormat(glInternalFormat, PixelFormatNormalizeOptionBit::None, + &realInternalFormat, &format, &type); + const SizeT readBytesPerTexel = + MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (readBytesPerTexel != bytesPerTexel) { + MGLOG_I("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller, + bytesPerTexel, readBytesPerTexel); + return false; + } + + Vector scratch(static_cast(width) * static_cast(height) * bytesPerTexel); + { + ScopedNeutralPackState neutralPack; + MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data()); + } + + auto* destination = + static_cast(mipmapTexture->MapMipmapData(uploadTarget, static_cast(level))); + if (!destination) return false; + + const SizeT fullRowBytes = static_cast(texelSize.x()) * bytesPerTexel; + const SizeT fullSliceBytes = static_cast(texelSize.y()) * fullRowBytes; + const SizeT copyRowBytes = static_cast(width) * bytesPerTexel; + for (GLsizei row = 0; row < height; ++row) { + Uint8* dst = destination + static_cast(zoffset) * fullSliceBytes + + static_cast(yoffset + row) * fullRowBytes + + static_cast(xoffset) * bytesPerTexel; + Memcpy(dst, scratch.data() + static_cast(row) * copyRowBytes, copyRowBytes); + } + mipmapTexture->MarkStorageDirty(uploadTarget, static_cast(level), true); + return true; + } } // namespace void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) { @@ -4798,10 +4888,8 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return; - // NOTE: the copy itself is still missing - CopyTexSubImage1D_State is a no-op and no backend - // exposes a 1D blit - so a valid call reaches the destination unchanged. Only the error - // reporting above is complete. - CopyTexSubImage1D_State(GL_TEXTURE_1D, level, xoffset, x, y, width); + CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset, + /*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__); } void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, @@ -4823,10 +4911,17 @@ namespace MobileGL::MG_Impl::GLImpl { if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, __func__)) { return; } - // NOTE: as with the 1D form, CopyTexSubImage3D_State does not perform the copy yet. - WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) { - CopyTexSubImage3D_State(glTarget, level, xoffset, yoffset, zoffset, x, y, width, height); - }); + // A cube map addresses its faces as separate upload targets, so zoffset selects the target + // rather than a slice within one; every other layered target keeps zoffset as the slice. + TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject); + GLint sliceOffset = zoffset; + if (target == TextureTarget::TextureCubeMap) { + uploadTarget = static_cast( + static_cast(TextureUploadTarget::CubeMapPositiveX) + static_cast(zoffset)); + sliceOffset = 0; + } + CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y, + width, height, __func__); } void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 48ffa047..332a3cd4 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -288,6 +288,35 @@ namespace MobileGL { return TextureInternalFormat::SRGB8; case GL_COMPRESSED_SRGB_ALPHA: return TextureInternalFormat::SRGB8Alpha8; + // BPTC and ETC2/EAC follow the same deviation as RGTC above, for the same reason: they + // are specific formats core GL requires (BPTC from 4.2, ETC2/EAC from 4.3) that this + // stack has no compressor for. INVALID_ENUM was never a legal answer for them, and + // uncompressed storage is the same trade the RGTC formats already take. + case GL_COMPRESSED_RGBA_BPTC_UNORM: + return TextureInternalFormat::RGBA8; + case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: + return TextureInternalFormat::SRGB8Alpha8; + case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT: + case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: + return TextureInternalFormat::RGB16F; + case GL_COMPRESSED_RGB8_ETC2: + return TextureInternalFormat::RGB8; + case GL_COMPRESSED_SRGB8_ETC2: + return TextureInternalFormat::SRGB8; + case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_RGBA8_ETC2_EAC: + return TextureInternalFormat::RGBA8; + case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + return TextureInternalFormat::SRGB8Alpha8; + case GL_COMPRESSED_R11_EAC: + return TextureInternalFormat::R8; + case GL_COMPRESSED_SIGNED_R11_EAC: + return TextureInternalFormat::R8Snorm; + case GL_COMPRESSED_RG11_EAC: + return TextureInternalFormat::RG8; + case GL_COMPRESSED_SIGNED_RG11_EAC: + return TextureInternalFormat::RG8Snorm; case GL_ALPHA: case GL_RED: return TextureInternalFormat::Red;