From 48968a663fdce9fa389f9488136248d3eb46a8c7 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:49:26 -0400 Subject: [PATCH 1/3] [Fix, Test] (GLImpl, Util): let a buffer clear take a GL_INT pattern into a normalized format --- MobileGL/MG_Test/Buffer/BufferTest.cpp | 56 +++++++++++++++++++ .../MG_Util/Texture/PixelStoreProcessor.cpp | 24 ++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 7a463b9c..61cdc560 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -711,6 +711,62 @@ TEST_F(BufferTest, ClearNamedBufferSubDataRepeatsPattern) { EXPECT_EQ(actual, (Vector{0, pattern, pattern, pattern, 0})); EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// GL 4.6 core table 8.2 pairs GL_INT with the non-integer base formats as a signed-normalized +// source, so a GL_R8 clear whose pattern arrives as (GL_RED, GL_INT) is legal. The pair used to be +// rejected with INVALID_VALUE, which is the first call +// KHR-GL45.direct_state_access.buffers_functional makes. +TEST_F(BufferTest, ClearNamedBufferSubDataAcceptsSignedNormalizedIntPattern) { + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::CreateBuffers(1, &buffer); + + const Vector initial(24, 0x7F); + MobileGL::MG_Impl::GLImpl::NamedBufferStorage( + buffer, initial.size(), initial.data(), + GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT | GL_MAP_PERSISTENT_BIT); + ASSERT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const GLint zero = 0; + MobileGL::MG_Impl::GLImpl::ClearNamedBufferSubData(buffer, GL_R8, 0, sizeof(GLint), GL_RED, GL_INT, &zero); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Vector actual(initial.size()); + auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer); + ASSERT_NE(bufferObject, nullptr); + Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size()); + Vector expected(initial); + for (SizeT i = 0; i < sizeof(GLint); ++i) expected[i] = 0; + EXPECT_EQ(actual, expected); + + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + +// The same pair on the bound-target entry point: the DSA and the bound call share +// ClearBufferRange_State, and a regression in either direction has to show up here too. +TEST_F(BufferTest, ClearBufferSubDataAcceptsSignedNormalizedIntPattern) { + GLuint buffer = 0; + MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer); + + const Vector initial(8, 0x7F); + MobileGL::MG_Impl::GLImpl::BufferData(GL_ARRAY_BUFFER, initial.size(), initial.data(), GL_STATIC_DRAW); + // GL_INT is signed-normalized against 2^31-1, so the maximum maps to a saturated GL_R8 texel. + const GLint one = 2147483647; + MobileGL::MG_Impl::GLImpl::ClearBufferSubData(GL_ARRAY_BUFFER, GL_R8, 0, 4, GL_RED, GL_INT, &one); + EXPECT_EQ(MobileGL::MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Vector actual(initial.size()); + auto bufferObject = MobileGL::MG_State::pGLContext->GetBufferObject(buffer); + ASSERT_NE(bufferObject, nullptr); + Memcpy(actual.data(), bufferObject->AcquireMemory(false, true, false), actual.size()); + EXPECT_EQ(actual, (Vector{0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x7F, 0x7F, 0x7F})); + + MobileGL::MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, 0); + MobileGL::MG_Impl::GLImpl::DeleteBuffers(1, &buffer); + DrainPendingGlErrors(); +} + TEST_F(BufferTest, ClearBufferSubDataInitializesIrisStaticSsboRange) { GLuint buffer = 0; MobileGL::MG_Impl::GLImpl::GenBuffers(1, &buffer); diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index 62c633ba..a698c4cf 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -442,14 +442,30 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { return packed.fieldCount == mapping.channelCount; } + // GL 4.6 core table 8.2: every unpacked component type pairs with every base format, + // with only two exclusions - an integer format takes integer types only, and the two + // floating types need a non-integer format. This used to be derived from + // GetDirectShadowComponentForType, which answers a different question (is the client + // layout byte-identical to some shadow layout) and has no SNorm32 to hand back for + // (non-integer format, GL_INT). That legal pair was therefore rejected outright, even + // though ConvertUnpackRow decodes it through DecodeComponentToFloat like every other + // normalized type - which is what glClearBufferData(GL_R8, GL_RED, GL_INT) needs. switch (type) { case TexturePixelDataType::UnsignedInt5999Rev: case TexturePixelDataType::UnsignedInt101111Rev: return !mapping.isInteger && mapping.channelCount == 3; - default: { - ShadowComponent component{}; - return GetDirectShadowComponentForType(type, mapping.isInteger, component); - } + case TexturePixelDataType::UnsignedByte: + case TexturePixelDataType::Byte: + case TexturePixelDataType::UnsignedShort: + case TexturePixelDataType::Short: + case TexturePixelDataType::UnsignedInt: + case TexturePixelDataType::Int: + return true; + case TexturePixelDataType::HalfFloat: + case TexturePixelDataType::Float: + return !mapping.isInteger; + default: + return false; } } From f559d6872877525803cdf8b03663add9c3689fc3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:49:33 -0400 Subject: [PATCH 2/3] [Feat, Test] (GLImpl): store and patch compressed 3D texture images, and wire the 1D/3D named entry points --- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 8 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 256 +++++++++++++++++- MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h | 5 + MobileGL/MG_Test/Texture/TextureTest.cpp | 142 ++++++++++ 4 files changed, 402 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index 0eb0bd91..e9c3cd48 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -1047,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) @@ -1835,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits) -DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits) +DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index a8c841bb..ad440012 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -4099,11 +4099,171 @@ namespace MobileGL::MG_Impl::GLImpl { "1D textures are not supported by this implementation")); } + // The three-dimensional twin of CompressedTexSubImage2D_State: a block-aligned box of the + // compressed image the level shadows is replaced, slice by slice. Same deviation as the 2D form + // - the uncompressed texel shadow beside it is NOT touched, so what changes is the image + // glGetCompressedTexImage hands back, not what the level samples as. void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) { - // TODO: implement compressed upload - see CompressedTexImage2D_State. - RecordUnsupportedCompressedFormat(__func__); + // ======================= Converting ================================ + const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); + const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + // Zero block width doubles as "format is not a specific compressed format", the + // INVALID_ENUM case - one lookup answers both questions. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format); + + // ===================== Error Checking ============================== + if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; + // A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair + // accepts one. + if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "A proxy target has no texture image to modify.")); + return; + } + if (!TextureImpl::ValidateTextureLevelNumber(level)) return; + if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; + if (width < 0 || height < 0 || depth < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "width, height and depth must be non-negative.")); + return; + } + if (compressedInfo.blockWidth == 0) { + RecordUnsupportedCompressedFormat(__func__); + return; + } + + auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; + auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + if (textureMipmapObject == nullptr) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed.")); + return; + } + // GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in + // exactly this compressed format. That is also what makes the block arithmetic below + // sound - the level's grid is measured with THIS format's block size. + const GLenum levelFormat = + textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast(level)); + if (levelFormat != format) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "format does not match the internal format of the texture image.")); + return; + } + + const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast(level)); + const Int levelDepth = std::max(levelSize.z(), 1); + // Subtractions rather than sums for the reason CompressedTexSubImage2D_State spells out: + // offset + extent are both application-supplied GLints and a signed overflow is undefined. + if (xoffset < 0 || yoffset < 0 || zoffset < 0 || width > levelSize.x() - xoffset || + height > levelSize.y() - yoffset || depth > levelDepth - zoffset) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "The replaced region does not lie within the texture image.")); + return; + } + // GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary + // and must either be a whole number of blocks wide/high or run to the image's edge. Every + // format that reaches here is 4x4x1, so the depth axis carries no block alignment rule - + // each slice is its own block grid. + const Int blockWidth = static_cast(compressedInfo.blockWidth); + const Int blockHeight = static_cast(compressedInfo.blockHeight); + const Bool alignedX = (xoffset % blockWidth == 0) && + (width % blockWidth == 0 || xoffset + width == levelSize.x()); + const Bool alignedY = (yoffset % blockHeight == 0) && + (height % blockHeight == 0 || yoffset + height == levelSize.y()); + if (!alignedX || !alignedY) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "The replaced region is not aligned to the format's compressed blocks.")); + return; + } + // Exactly the size the format and dimensions imply, which is also what keeps the copy + // below in bounds. + const SizeT expectedImageSize = + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}); + if (imageSize < 0 || static_cast(imageSize) != expectedImageSize) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "imageSize does not match the compressed image size.")); + return; + } + + // ======================= Processing ================================ + if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return; + const void* compressedBytes = CompressedUnpackSource(data); + if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op + if (compressedBytes == nullptr) { + // No unpack buffer and a null client pointer: there is nothing to read. GL leaves + // this undefined rather than erroring, and dereferencing it is the one answer that + // is never acceptable. + MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__); + return; + } + + static std::atomic announcedNoCodec3D{false}; + if (!announcedNoCodec3D.exchange(true)) { + MGLOG_W("%s: the compressed blocks are stored verbatim and returned by " + "glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not " + "reach the texels this level SAMPLES as. Upload through glTexSubImage3D for " + "that.", + __func__); + } + + const SizeT blobSize = + textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast(level)); + const void* existing = + textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast(level)); + if (blobSize == 0 || existing == nullptr) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "The texture level holds no compressed image to modify.")); + return; + } + Vector blob(blobSize); + Memcpy(blob.data(), existing, blobSize); + + const SizeT blockByteSize = compressedInfo.blockByteSize; + const SizeT levelBlocksX = (static_cast(levelSize.x()) + compressedInfo.blockWidth - 1) / + compressedInfo.blockWidth; + const SizeT levelBlocksY = (static_cast(levelSize.y()) + compressedInfo.blockHeight - 1) / + compressedInfo.blockHeight; + const SizeT levelRowBytes = levelBlocksX * blockByteSize; + const SizeT levelSliceBytes = levelRowBytes * levelBlocksY; + const SizeT regionBlocksX = (static_cast(width) + compressedInfo.blockWidth - 1) / + compressedInfo.blockWidth; + const SizeT regionBlocksY = (static_cast(height) + compressedInfo.blockHeight - 1) / + compressedInfo.blockHeight; + const SizeT firstBlockX = static_cast(xoffset) / compressedInfo.blockWidth; + const SizeT firstBlockY = static_cast(yoffset) / compressedInfo.blockHeight; + const SizeT regionRowBytes = regionBlocksX * blockByteSize; + const SizeT regionSliceBytes = regionRowBytes * regionBlocksY; + const auto* source = static_cast(compressedBytes); + for (SizeT slice = 0; slice < static_cast(depth); ++slice) { + const SizeT destSliceBase = (static_cast(zoffset) + slice) * levelSliceBytes; + for (SizeT row = 0; row < regionBlocksY; ++row) { + const SizeT destOffset = + destSliceBase + (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize; + if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size + Memcpy(blob.data() + destOffset, source + slice * regionSliceBytes + row * regionRowBytes, + regionRowBytes); + } + } + textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast(level), format, + blob.data(), blobSize); } // Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a @@ -4278,15 +4438,83 @@ namespace MobileGL::MG_Impl::GLImpl { RecordUnsupportedCompressedFormat(__func__); } + // The three-dimensional twin of CompressedTexImage2D_State, and the same deviation applies: the + // blocks are shadowed verbatim for glGetCompressedTexImage while the texels this level SAMPLES + // as stay zero, because there is no BC/ETC decoder here. A 3D compressed image is a stack of + // `depth` two-dimensional block grids - every format that reaches here has a 4x4x1 block - so + // the blob layout is slice-major and CalculateCompressedTextureImageSize already multiplies by + // depth. void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* data) { + // ======================= Converting ================================ const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); - auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + // Zero block width doubles as "internalformat is not a specific compressed format", which is + // the INVALID_ENUM case - one lookup answers both questions. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat); + + // ===================== Error Checking ============================== + if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; + if (!TextureImpl::ValidateTextureLevelNumber(level)) return; + if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return; + if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return; + if (!TextureImpl::ValidateTextureBorderNumber(border)) return; + if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; + if (compressedInfo.blockWidth == 0) { + RecordUnsupportedCompressedFormat(__func__); + return; + } + // GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply, + // otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds. + const SizeT expectedImageSize = + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}); + if (imageSize < 0 || static_cast(imageSize) != expectedImageSize) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "imageSize does not match the compressed image size.")); + return; + } + + // Object resolution copied from TexImage3D_State rather than routed through + // GetTextureObjectByTarget, for the reason CompressedTexImage2D_State gives: a proxy target + // is legal here and only CreateOrReplaceProxyTextureObject gives it an object to answer the + // level queries from. + auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); + auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); + const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget); + auto& textureObject = + isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget) + : bindingSlot.GetBoundObject(); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; if (!ValidateTextureMutable(textureObject, __func__)) return; - // TODO: implement compressed upload - see CompressedTexImage2D_State. - RecordUnsupportedCompressedFormat(__func__); + // ======================= Processing ================================ + const TextureInternalFormat textureInternalFormat = + MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat); + textureObject->SetInternalFormat(textureInternalFormat); + + // A proxy records the format and nothing else - it must never take storage, and it must never + // be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being + // INVALID_OPERATION. + if (isProxy) return; + + const SizeT internalBpp = + MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte); + const SizeT internalBytes = + static_cast(width) * static_cast(height) * static_cast(depth) * internalBpp; + + auto* textureMipmapObject = static_cast(textureObject.get()); + DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); + // AllocateStorage clears any compressed image the level used to hold, so this must run before + // SetMipmapCompressedImage re-arms it. + textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); + + if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return; + const void* compressedBytes = CompressedUnpackSource(data); + textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes, + expectedImageSize); + textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); } void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, @@ -5521,6 +5749,14 @@ namespace MobileGL::MG_Impl::GLImpl { free(processedPixels); } + void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, + GLsizei imageSize, const void* data) { + auto textureObject = GetTextureObjectByName(texture, __func__); + WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { + CompressedTexSubImage1D_State(target, level, xoffset, width, format, imageSize, data); + }); + } + void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) { auto textureObject = GetTextureObjectByName(texture, __func__); @@ -5529,6 +5765,16 @@ namespace MobileGL::MG_Impl::GLImpl { }); } + void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, + GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, + const void* data) { + auto textureObject = GetTextureObjectByName(texture, __func__); + WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { + CompressedTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format, + imageSize, data); + }); + } + void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) { auto textureObject = GetTextureObjectByName(texture, __func__); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index 5ccd0190..4a39f405 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -37,8 +37,13 @@ namespace MobileGL::MG_Impl::GLImpl { GLenum format, GLenum type, const void* pixels); void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels); + void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, + GLsizei imageSize, const void* data); void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data); + void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, + GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, + const void* data); void TextureParameterf(GLuint texture, GLenum pname, GLfloat param); void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params); void TextureParameteri(GLuint texture, GLenum pname, GLint param); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 06562507..4ed235e6 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -2227,6 +2227,148 @@ TEST_F(TextureTest, CompressedTextureSubImage2DModifiesTheNamedTextureOnly) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +namespace { + // 8x8x8 RGTC1: 2x2 blocks of 8 bytes per slice, so a slice is 32 bytes and the stack is 256. + constexpr GLsizei kRgtc1Size8x8x8 = 256; + constexpr GLsizei kRgtc1Slice8x8 = 32; + + GLuint MakeCompressedRgtc1Texture3D() { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8, + nullptr); + return texture; + } +} // namespace + +// glCompressedTexImage3D used to answer GL_INVALID_ENUM to every call, which is what threw +// KHR-GL45.direct_state_access.textures_compressed_subimage out with an InternalError: the CTS +// asserts no error on it. A 3D compressed image is a stack of per-slice block grids, and the whole +// stack has to come back byte for byte. +TEST_F(TextureTest, CompressedTexImage3DShadowsTheWholeStackForReadback) { + Uint8 whole[kRgtc1Size8x8x8]; + for (Int i = 0; i < kRgtc1Size8x8x8; ++i) whole[i] = static_cast(i); + + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, texture); + MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Size8x8x8, + whole); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 stored[kRgtc1Size8x8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored); + EXPECT_EQ(std::memcmp(stored, whole, sizeof(whole)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // An imageSize that is not the one the format and the three dimensions imply - the depth axis + // is the term a 2D-shaped size calculation would drop. + MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_3D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 8, 0, kRgtc1Slice8x8, + whole); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +// Where the incoming blocks land. The box below is one block wide, one block high and two slices +// deep, starting at block (1,1) of slice 3: an implementation that dropped the slice stride, the +// block-row term or the block-column term puts them somewhere else, and a full-image write would +// hide all three. +TEST_F(TextureTest, CompressedTexSubImage3DPlacesBlocksSliceBySlice) { + const GLuint texture = MakeCompressedRgtc1Texture3D(); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 zeros[kRgtc1Size8x8x8] = {}; + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1, + kRgtc1Size8x8x8, zeros); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const Uint8 box[16] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7}; + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 4, 4, 3, 4, 4, 2, GL_COMPRESSED_RED_RGTC1, + static_cast(sizeof(box)), box); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 expected[kRgtc1Size8x8x8] = {}; + // slice 3, block row 1, block column 1 -> 3*32 + 1*16 + 1*8, and the same place one slice on. + std::memcpy(expected + 3 * kRgtc1Slice8x8 + 16 + 8, box, 8); + std::memcpy(expected + 4 * kRgtc1Slice8x8 + 16 + 8, box + 8, 8); + + Uint8 stored[kRgtc1Size8x8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored); + EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// glCompressedTextureSubImage3D was an exported no-op that raised no error at all. It must reach the +// NAMED texture and leave the binding it borrowed exactly as it found it. +TEST_F(TextureTest, CompressedTextureSubImage3DModifiesTheNamedTextureOnly) { + const GLuint bound = MakeCompressedRgtc1Texture3D(); + Uint8 boundImage[kRgtc1Size8x8x8]; + std::memset(boundImage, 0x11, sizeof(boundImage)); + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1, + kRgtc1Size8x8x8, boundImage); + + const GLuint named = MakeCompressedRgtc1Texture3D(); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, bound); // `named` is NOT the bound texture + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 namedImage[kRgtc1Size8x8x8]; + std::memset(namedImage, 0x22, sizeof(namedImage)); + MG_Impl::GLImpl::CompressedTextureSubImage3D(named, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1, + kRgtc1Size8x8x8, namedImage); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 stored[kRgtc1Size8x8x8] = {}; + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored); + EXPECT_EQ(std::memcmp(stored, boundImage, sizeof(stored)), 0); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, named); + std::memset(stored, 0, sizeof(stored)); + MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_3D, 0, stored); + EXPECT_EQ(std::memcmp(stored, namedImage, sizeof(stored)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, CompressedTexSubImage3DRejectsTheRegionsGLForbids) { + const GLuint texture = MakeCompressedRgtc1Texture3D(); + Uint8 blocks[kRgtc1Size8x8x8] = {}; + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A format that is not the one the image is stored in. + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RG_RGTC2, 512, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A start that is not on a block boundary. + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 2, 0, 0, 4, 8, 8, GL_COMPRESSED_RED_RGTC1, 128, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A box that runs past the last slice - the depth bound a 2D-shaped range check never applies. + MG_Impl::GLImpl::CompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 6, 8, 8, 4, GL_COMPRESSED_RED_RGTC1, 128, blocks); + ExpectSingleGlError(GL_INVALID_VALUE); + + (void)texture; +} + +// Core GL defines no compressed format for a 1D target, so both the bound and the by-name entry +// point have to REFUSE the call. The by-name one used to be an exported no-op that raised nothing, +// which is the one answer an application cannot act on. +TEST_F(TextureTest, CompressedTextureSubImage1DRefusesLikeTheBoundCall) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, texture); + MG_Impl::GLImpl::TexImage1D(GL_TEXTURE_1D, 0, GL_R8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + DrainPendingGlErrors(); + + Uint8 blocks[16] = {}; + MG_Impl::GLImpl::CompressedTexSubImage1D(GL_TEXTURE_1D, 0, 0, 8, GL_COMPRESSED_RED_RGTC1, + static_cast(sizeof(blocks)), blocks); + ExpectSingleGlError(GL_INVALID_ENUM); + + MG_Impl::GLImpl::CompressedTextureSubImage1D(texture, 0, 0, 8, GL_COMPRESSED_RED_RGTC1, + static_cast(sizeof(blocks)), blocks); + ExpectSingleGlError(GL_INVALID_ENUM); +} + TEST_F(TextureTest, CompressedTexSubImage2DRejectsTheRegionsGLForbids) { const GLuint texture = MakeCompressedRgtc1Texture8x8(); Uint8 blocks[kRgtc1Size8x8] = {}; From 26e5a946acd442ec35508bbd4eb3d1b166f10943 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 22 Aug 2026 21:56:51 -0400 Subject: [PATCH 3/3] [Test] (GLImpl): pin the DSA name rule on the compressed 3D by-name entry point --- MobileGL/MG_Test/Texture/TextureTest.cpp | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 4ed235e6..3bbbac41 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -2349,6 +2349,33 @@ TEST_F(TextureTest, CompressedTexSubImage3DRejectsTheRegionsGLForbids) { (void)texture; } +// The DSA name rule the CTS's textures_creation pair does not reach for these two entry points: a +// name handed out by glGenTextures has no object until it is first bound, so a by-name call on it is +// INVALID_OPERATION - and, unlike the stub these replaced, it has to SAY so rather than return +// quietly. A glCreateTextures name is a created object and gets past the name check. +TEST_F(TextureTest, CompressedTextureSubImage3DRejectsAGeneratedButNeverBoundName) { + GLuint generated = 0; + MG_Impl::GLImpl::GenTextures(1, &generated); + ASSERT_NE(generated, 0u); + DrainPendingGlErrors(); + + Uint8 blocks[kRgtc1Size8x8x8] = {}; + MG_Impl::GLImpl::CompressedTextureSubImage3D(generated, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1, + kRgtc1Size8x8x8, blocks); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A created name is past the name check, so whatever it answers is about the IMAGE (this one + // holds none yet), never about the name. + GLuint created = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &created); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::CompressedTextureSubImage3D(created, 0, 0, 0, 0, 8, 8, 8, GL_COMPRESSED_RED_RGTC1, + kRgtc1Size8x8x8, blocks); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION) + << "a created 3D texture with no compressed image is an image error, not a name error"; + DrainPendingGlErrors(); +} + // Core GL defines no compressed format for a 1D target, so both the bound and the by-name entry // point have to REFUSE the call. The by-name one used to be an exported no-op that raised nothing, // which is the one answer an application cannot act on.