From 0e7692251d73084aa23d2303e6d67931bacbcc3b Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Wed, 5 Aug 2026 09:40:29 -0400 Subject: [PATCH] [Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so direct_state_access.textures_get_image threw at its first compressed call and reported InternalError with nothing in the log at all - the uncompressed half of the case had already passed. The compressed bytes are now kept verbatim, in a side-channel beside the texel shadow rather than in place of it. That placement is the load-bearing decision: both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte size by the texel count to recover bytes-per-texel, so putting 16 bytes where a 4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel storage therefore stays uncompressed and correctly sized - the image samples as zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of ConvertGLEnumToTextureInternalFormat already document - while glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could satisfy byte for byte. Nothing ever hands the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather than potentially stale, which is why the readback never asks a backend. The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back with uncompressed storage, so the upload can never accept a format whose texel shadow it cannot allocate. imageSize is checked against the block arithmetic, which is also what keeps the copy in bounds. Three things the shape depends on. AllocateStorage clears the compressed tag, so a glTexImage2D or glTexStorage2D over the level un-compresses it - without that, textures_compressed_subimage would flip branches and start asking for data MobileGL cannot produce. GL_TEXTURE_COMPRESSED and GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture, because a compressed internalformat handed to glTexImage2D resolves to uncompressed storage and must keep reading as uncompressed. And GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true. Still rejected on purpose: glCompressedTexImage1D/3D and every glCompressedTexSubImage*, which caps the blast radius. Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of KHR-GL45 is identical before and after on both backends but for get_texture_sub_image.errors_test, which stops throwing and fails on a value instead. --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 254 +++++++++++++++--- .../GLState/TextureState/MipmapStorage.cpp | 43 +++ .../GLState/TextureState/MipmapStorage.h | 17 ++ .../TextureState/MipmapUploadTargetArray.h | 21 ++ .../GLState/TextureState/TextureObject.cpp | 21 ++ .../GLState/TextureState/TextureObject.h | 17 ++ .../TextureState/TextureObject2DCube.cpp | 21 ++ .../TextureState/TextureObject2DCube.h | 6 + MobileGL/MG_Util/Metrics/TextureMetrics.cpp | 45 ++++ MobileGL/MG_Util/Metrics/TextureMetrics.h | 18 ++ 10 files changed, 427 insertions(+), 36 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 96e260ae..dc8be44e 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -216,8 +216,23 @@ namespace MobileGL::MG_Impl::GLImpl { // compressed format up front, so no texture image MobileGL holds can be compressed. Written // as a predicate rather than a literal false so both level-parameter getters stay in step // once compressed formats do land. - Bool IsCompressedTextureFormat(TextureInternalFormat) { - return false; + // GL 4.6 core 8.11 asks "is *this level* stored compressed", not "is the texture's internal + // format a compressed one", and here the two genuinely differ: a compressed internalformat + // handed to glTexImage2D resolves to the uncompressed storage that backs it (see + // ConvertGLEnumToTextureInternalFormat), so the texture's format enum can never answer yes. + // The only levels stored compressed are the ones glCompressedTexImage* shadowed verbatim, + // which is exactly what the per-level compressed format records. + // + // No level-count guard on purpose: TextureObject2DCube::GetMipmapLevelCount() reports face + // zero's chain only, so a count check would answer GL_NONE for a compressed image on any + // other face - precisely the per-face independence the storage layer provides. MipmapStorage's + // own getters already bounds-check per target and return GL_NONE for an unallocated level. + GLenum GetCompressedLevelFormat(const SharedPtr& textureObject, + TextureUploadTarget uploadTarget, GLint level) { + if (!textureObject || level < 0) return GL_NONE; + const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + if (!textureMipmapObject) return GL_NONE; + return textureMipmapObject->GetMipmapCompressedFormat(uploadTarget, static_cast(level)); } GLint GetTextureLevelComponentParameter(TextureInternalFormat textureInternalFormat, GLenum pname) { @@ -538,10 +553,12 @@ namespace MobileGL::MG_Impl::GLImpl { texture->TruncateMipmapLevels(uploadTarget, 1); } - // Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS - // reports 0, so every compressed internalformat is by definition unsupported and - // GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds - // a C++ exception through the C GL ABI and takes the process down. + // The compressed internalformat is not one this stack can store (see + // MG_Util::GetCompressedFormatInfo for the accepted set: the RGTC/BPTC/ETC2-EAC formats core + // GL requires). GL_INVALID_ENUM is the specified error for an unsupported compressed format - + // unlike THROW_UNIMPL_EXCEPTION, which unwinds a C++ exception through the C GL ABI and takes + // the process down. Still the only outcome for the 1D/3D and sub-image entry points, which + // have no compressed upload path yet. void RecordUnsupportedCompressedFormat(const char* caller) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, @@ -2877,7 +2894,14 @@ namespace MobileGL::MG_Impl::GLImpl { break; case GL_TEXTURE_INTERNAL_FORMAT: if (params) { - *params = (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()); + // A level stored compressed must report the token it was given, not the + // uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets + // that tag, so every level created by glTexImage*D - including one given a compressed + // internalformat - still answers with its resolved storage format. + const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level); + *params = (compressedFormat != GL_NONE) + ? (GLint)compressedFormat + : (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()); } break; case GL_TEXTURE_SAMPLES: @@ -2907,14 +2931,16 @@ namespace MobileGL::MG_Impl::GLImpl { break; case GL_TEXTURE_COMPRESSED: if (params) { - *params = IsCompressedTextureFormat(textureObject->GetFormat()) ? GL_TRUE : GL_FALSE; + *params = + (GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? GL_TRUE + : GL_FALSE; } break; - case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: + case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: { // GL 4.6 core 8.11: there is no compressed size to report for an image whose internal // format is uncompressed, nor for a proxy target, and the query is INVALID_OPERATION // rather than a zero. - if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) { + if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( @@ -2923,9 +2949,12 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (params) { - *params = 0; + const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + *params = static_cast( + textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast(level))); } break; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexLevelParameteriv_State", @@ -3000,7 +3029,14 @@ namespace MobileGL::MG_Impl::GLImpl { break; case GL_TEXTURE_INTERNAL_FORMAT: if (params) { - *params = (GLfloat)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()); + // A level stored compressed must report the token it was given, not the + // uncompressed format backing it (GL 4.6 core 8.11). Only glCompressedTexImage* sets + // that tag, so every level created by glTexImage*D - including one given a compressed + // internalformat - still answers with its resolved storage format. + const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level); + *params = (GLfloat)((compressedFormat != GL_NONE) + ? compressedFormat + : MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat())); } break; case GL_TEXTURE_SAMPLES: @@ -3030,13 +3066,14 @@ namespace MobileGL::MG_Impl::GLImpl { break; case GL_TEXTURE_COMPRESSED: if (params) { - *params = IsCompressedTextureFormat(textureObject->GetFormat()) ? 1.0f : 0.0f; + *params = + (GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? 1.0f : 0.0f; } break; - case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: + case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: { // See GetTexLevelParameteriv_State: uncompressed images and proxy targets have no // compressed size to report, so GL 4.6 core 8.11 makes the query an error. - if (isProxy || !IsCompressedTextureFormat(textureObject->GetFormat())) { + if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( @@ -3045,9 +3082,12 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (params) { - *params = 0.0f; + const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + *params = static_cast( + textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast(level))); } break; + } default: MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "GetTexLevelParameterfv_State", @@ -3056,14 +3096,72 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the + // same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from + // the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target + // form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE), + // so only the DSA form passes a real bound. + void CopyCompressedTextureImageToClientOrPBO(const SharedPtr& textureObject, + TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize, + void* pixels, const char* caller) { + if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, + "Texture level is not stored in a compressed format.")); + return; + } + + const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get()); + const SizeT imageSize = + textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast(level)); + const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast(level)); + if (!src || imageSize == 0) return; + + if (bufSize >= 0 && static_cast(bufSize) < imageSize) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, "Destination buffer is too small.")); + return; + } + + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + if (pixelPackBufferObject) { + if (pixelPackBufferObject->IsMapped()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, "Pixel pack buffer is currently mapped.")); + return; + } + const SizeT offset = reinterpret_cast(pixels); + const SizeT bufferSize = pixelPackBufferObject->GetSize(); + if (offset > bufferSize || imageSize > bufferSize - offset) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, + "Packing would write past the end of the pixel pack buffer.")); + return; + } + pixelPackBufferObject->UploadSubData({const_cast(src), imageSize}, offset); + return; + } + + // No pixel-store packing here on purpose: GL 4.6 core 8.11 says the pixel storage modes are + // ignored for a compressed image, which is also the only way the round trip stays byte-exact. + if (pixels) Memcpy(pixels, src, imageSize); + } + void GetCompressedTexImage_State(GLenum target, GLint level, void* img) { - // TODO: implement compressed readback. Reporting success while writing nothing hands - // the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL - // specifies GL_INVALID_OPERATION when the bound level is not compressed. - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", __func__, - "Texture level is not stored in a compressed format.")); + const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); + const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + // ValidateTextureUploadTarget records InvalidEnum itself; wrapping it in a second RecordError + // would report one failure twice. + if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; + if (!TextureImpl::ValidateTextureLevelNumber(level)) return; + auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + if (!TextureImpl::ValidateTextureObject(textureObject)) return; + CopyCompressedTextureImageToClientOrPBO(textureObject, textureUploadTarget, level, -1, img, __func__); } void GenTextures_State(GLsizei n, GLuint* textures) { @@ -3314,16 +3412,102 @@ namespace MobileGL::MG_Impl::GLImpl { void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, 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, 1)) 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, 1}); + 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 TexImage2D_State rather than routed through + // GetTextureObjectByTarget: GL 4.6 core 8.7 lets a proxy target reach glCompressedTexImage2D, + // and only CreateOrReplaceProxyTextureObject gives the proxy a fresh 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. Until then report the spec error for an - // unsupported compressed format rather than throwing - a C++ exception unwinding - // through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is - // exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises. - RecordUnsupportedCompressedFormat(__func__); + // ======================= Processing ================================ + // Texel storage stays uncompressed, exactly the deviation the RGTC/BPTC/ETC2 arms of + // ConvertGLEnumToTextureInternalFormat already document: neither backend has a BC/ETC codec + // and TextureInternalFormat has no compressed enumerator, so the shadow keeps the "one + // format, N bytes per texel" layout the backend upload sizing, glGenerateMipmap's + // bytes-per-texel division and the pixel-store packer all rely on. The image therefore + // samples as zeros. The application's bytes are kept beside it so glGetCompressedTexImage can + // return the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could + // satisfy byte for byte. + 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) * 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, 1}, internalBytes}); + + const void* compressedBytes = data; + const auto& pixelUnpackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject(); + if (pixelUnpackBufferObject) { + if (pixelUnpackBufferObject->IsMapped()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Pixel unpack buffer is currently mapped.")); + return; + } + const SizeT offset = reinterpret_cast(data); + const SizeT bufferSize = pixelUnpackBufferObject->GetSize(); + if (offset > bufferSize || expectedImageSize > bufferSize - offset) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", __func__, + "Unpacking would read past the end of the pixel unpack buffer.")); + return; + } + compressedBytes = reinterpret_cast(pixelUnpackBufferObject->MappedData()) + offset; + } + textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes, + expectedImageSize); + textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); } void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, @@ -4273,13 +4457,11 @@ namespace MobileGL::MG_Impl::GLImpl { // texture would also fail the compressed check below. if (!TextureImpl::ValidateTextureLevelNumber(level)) return; - // No texture MobileGL holds is compressed (see IsCompressedTextureFormat), so this is the - // only outcome today. Reporting success while writing nothing would hand the caller stale - // memory with GL_NO_ERROR - the same reasoning as GetCompressedTexImage_State. - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", __func__, - "Texture level is not stored in a compressed format.")); + // Unlike glGetTextureImage this never asks a backend: the compressed image only ever exists + // in the CPU shadow (no backend was handed the compressed bytes at all), so the shadow is + // authoritative rather than potentially stale. + CopyCompressedTextureImageToClientOrPBO(textureObject, GetPrimaryUploadTarget(textureObject), level, bufSize, + pixels, __func__); } void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 93f94675..51005659 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -27,11 +27,52 @@ namespace MobileGL { m_texelSizes.reserve(std::bit_ceil(requiredLevelCount)); m_texelSizes.resize(requiredLevelCount); m_isDirty.resize(requiredLevelCount, false); + m_compressedData.resize(requiredLevelCount); + m_compressedFormats.resize(requiredLevelCount, GL_NONE); } m_texelSizes[level] = input.texelSize; auto& data = m_data[level]; data.resize(input.byteSize, 0); + + // Respecifying a level drops whatever compressed image it used to hold. Without this, + // a glTexImage2D or glTexStorage2D over a level a previous glCompressedTexImage2D had + // shadowed would leave GL_TEXTURE_COMPRESSED answering true and glGetCompressedTexImage + // handing back the stale blob. Every allocation path funnels through here, so clearing + // once covers all of them; the compressed path re-arms the tag immediately afterwards + // via SetCompressedImage. + m_compressedFormats[level] = GL_NONE; + m_compressedData[level].clear(); + m_compressedData[level].shrink_to_fit(); + } + + void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) { + MOBILEGL_ASSERT(level < m_compressedData.size(), "SetCompressedImage: level out of range"); + + m_compressedFormats[level] = internalFormat; + auto& blob = m_compressedData[level]; + // Zero-filled when data is null: glCompressedTexImage* with a null pointer defines the + // level's size and format but leaves its contents undefined, and zeros are the one + // reproducible answer a later glGetCompressedTexImage can give. + blob.assign(size, 0); + if (data != nullptr && size > 0) { + Memcpy(blob.data(), data, size); + } + } + + GLenum MipmapStorage::GetCompressedFormat(Uint level) const { + if (level >= m_compressedFormats.size()) return GL_NONE; + return m_compressedFormats[level]; + } + + SizeT MipmapStorage::GetCompressedByteSize(Uint level) const { + if (level >= m_compressedData.size()) return 0; + return m_compressedData[level].size(); + } + + const void* MipmapStorage::MapCompressedData(Uint level) const { + if (level >= m_compressedData.size()) return nullptr; + return m_compressedData[level].data(); } void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { @@ -40,6 +81,8 @@ namespace MobileGL { m_data.resize(levelCount); m_texelSizes.resize(levelCount); m_isDirty.resize(levelCount); + m_compressedData.resize(levelCount); + m_compressedFormats.resize(levelCount); } void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index 61c1bc9d..2f7761d1 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -30,10 +30,27 @@ namespace MobileGL { void MarkDirty(Uint level, bool dirty); bool IsDirty(Uint level) const; + // The bytes an application handed to glCompressedTexImage*, kept verbatim beside the + // (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires + // glGetCompressedTexImage to return the image *as stored*, and no backend here has a + // BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has + // to keep the "width * height * bytes-per-texel" layout that the backend upload + // sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all + // divide by. Two parallel vectors, one invariant preserved. Call order is + // AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain + // glTexImage2D over the level un-compresses it. + void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size); + // GL_NONE when the level is not stored compressed. + GLenum GetCompressedFormat(Uint level) const; + SizeT GetCompressedByteSize(Uint level) const; + const void* MapCompressedData(Uint level) const; + protected: Vector m_texelSizes; Vector> m_data; Vector m_isDirty; + Vector> m_compressedData; + Vector m_compressedFormats; }; } // namespace GLState } // namespace MG_State diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 9e452271..3b0ff28b 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -74,6 +74,27 @@ namespace MobileGL { return m_storage[targetIndex].IsDirty(level); } + void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data, + SizeT size) { + MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid"); + m_storage[targetIndex].SetCompressedImage(level, internalFormat, data, size); + } + + GLenum GetCompressedFormat(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedFormat: target invalid"); + return m_storage[targetIndex].GetCompressedFormat(level); + } + + SizeT GetCompressedByteSize(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetCompressedByteSize: target invalid"); + return m_storage[targetIndex].GetCompressedByteSize(level); + } + + const void* MapCompressedData(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "MapCompressedData: target invalid"); + return m_storage[targetIndex].MapCompressedData(level); + } + protected: Array m_storage; }; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 7e6110df..5d1f8c1c 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -301,6 +301,27 @@ namespace MobileGL { return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat, const void* data, SizeT size) { + m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat, data, size); + } + + GLenum TextureObjectWithOneMipmap::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + + SizeT TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + + const void* TextureObjectWithOneMipmap::MapMipmapCompressedImage(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const { if (m_textureStorage.GetLevelCount() == 0) { return {0, 0, 0}; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 73440ac8..15d7c362 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -150,6 +150,18 @@ namespace MobileGL::MG_State::GLState { virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0; virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0; virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + + // The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim + // next to the texel data rather than instead of it - see MipmapStorage. The texel shadow + // stays uncompressed and correctly sized, so nothing in the backend upload path has to know + // these exist; only glGetCompressedTexImage, glGetCompressedTextureImage and the + // GL_TEXTURE_COMPRESSED* level queries read them. + virtual void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat, const void* data, SizeT size) = 0; + // GL_NONE when this level is not stored compressed. + virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; }; // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the @@ -206,6 +218,11 @@ namespace MobileGL::MG_State::GLState { void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override; bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, + const void* data, SizeT size) override; + GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 9d6b0e66..4a88af09 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -55,6 +55,27 @@ namespace MobileGL { return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat, const void* data, SizeT size) { + m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat, data, size); + } + + GLenum TextureObject2DCube::GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + + SizeT TextureObject2DCube::GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetCompressedByteSize(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + + const void* TextureObject2DCube::MapMipmapCompressedImage(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const { MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target && target <= TextureUploadTarget::CubeMapNegativeZ, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index e1f2f5ce..5f9ef831 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -27,6 +27,12 @@ namespace MobileGL { void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override; bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat, const void* data, SizeT size) override; + GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp index 4e9d5ac0..2c9f8b08 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.cpp +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.cpp @@ -525,5 +525,50 @@ namespace MobileGL { return s; } + CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat) { + // Every format here is 4x4-blocked; only the bytes per block differ (8 for the one- and + // two-channel RGTC/EAC and the 1-bit-alpha ETC2 forms, 16 for BPTC and the full-alpha + // ETC2/EAC and two-channel EAC forms). The generic compressed formats (GL_COMPRESSED_RGBA + // and friends) are deliberately absent: GL lets the implementation pick, MobileGL picks + // uncompressed, and glCompressedTexImage* must reject them because there is no defined + // block layout to hand it. The accepted set is exactly the set + // ConvertGLEnumToTextureInternalFormat can back with uncompressed storage, so this call + // can never accept a format whose texel shadow cannot be allocated. + switch (internalFormat) { + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_SIGNED_RED_RGTC1: + case GL_COMPRESSED_RGB8_ETC2: + case GL_COMPRESSED_SRGB8_ETC2: + case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_R11_EAC: + case GL_COMPRESSED_SIGNED_R11_EAC: + return {4, 4, 8}; + case GL_COMPRESSED_RG_RGTC2: + case GL_COMPRESSED_SIGNED_RG_RGTC2: + case GL_COMPRESSED_RGBA_BPTC_UNORM: + case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: + case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT: + case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: + case GL_COMPRESSED_RGBA8_ETC2_EAC: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + case GL_COMPRESSED_RG11_EAC: + case GL_COMPRESSED_SIGNED_RG11_EAC: + return {4, 4, 16}; + default: + return {}; + } + } + + SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size) { + if (info.blockWidth == 0 || info.blockHeight == 0) return 0; + const SizeT width = static_cast(std::max(size.x(), 0)); + const SizeT height = static_cast(std::max(size.y(), 0)); + const SizeT depth = static_cast(std::max(size.z(), 1)); + const SizeT blocksX = (width + info.blockWidth - 1) / info.blockWidth; + const SizeT blocksY = (height + info.blockHeight - 1) / info.blockHeight; + return blocksX * blocksY * depth * info.blockByteSize; + } + } // namespace MG_Util } // namespace MobileGL diff --git a/MobileGL/MG_Util/Metrics/TextureMetrics.h b/MobileGL/MG_Util/Metrics/TextureMetrics.h index cd99ee0d..18e42b60 100644 --- a/MobileGL/MG_Util/Metrics/TextureMetrics.h +++ b/MobileGL/MG_Util/Metrics/TextureMetrics.h @@ -24,5 +24,23 @@ namespace MobileGL { SizeT CalculateInputTextureImageSize(TextureInputFormat inputFormat, TexturePixelDataType pixelDataType, IntVec3 size); ComponentSizes GetComponentSizesForInternalFormat(TextureInternalFormat internal); + + // A specific compressed internal format described the only two ways the CPU shadow needs it: + // the texel footprint of one block and the bytes that block occupies. Kept as a GLenum query + // rather than a TextureInternalFormat one on purpose - TextureInternalFormat has no + // compressed enumerator (a compressed format resolves to the uncompressed storage backing + // it), so by the time the enum has been converted the block geometry is gone. + struct CompressedFormatInfo { + // Zero means "internalformat is not one of the specific compressed formats core GL + // requires" - which is exactly the glCompressedTexImage* INVALID_ENUM case, so callers + // get the format test and the block geometry from one lookup. + Uint blockWidth = 0; + Uint blockHeight = 0; + SizeT blockByteSize = 0; + }; + CompressedFormatInfo GetCompressedFormatInfo(GLenum internalFormat); + // Blocks are counted rounded up, exactly as GL 4.6 core 8.7 sizes a compressed image, so a + // 4x4 BPTC image is one 16-byte block and a 1x1 one still is. + SizeT CalculateCompressedTextureImageSize(const CompressedFormatInfo& info, IntVec3 size); } // namespace MG_Util } // namespace MobileGL