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;