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