// MobileGL - MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header #include "Validators.h" #include #include #include #include #include #include #include #include #include namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTextureTarget(TextureTarget target) { if (target == TextureTarget::Unknown) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateTextureTarget", "Invalid texture target")); return false; } return true; } Bool ValidateTextureUploadTarget(TextureUploadTarget textureUploadTarget) { if (textureUploadTarget == TextureUploadTarget::Unknown) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateTextureUploadTarget", "Invalid texture upload target")); return false; } return true; } Bool ValidateTextureName(Uint texture, Bool allowZero) { if (texture == 0) { if (allowZero) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureName", "Texture name cannot be zero")); return false; } if (!MG_State::pGLContext->ValidateTextureName(texture)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureName", "Invalid texture name")); return false; } return true; } Bool ValidateTextureInputFormat(TextureInputFormat format) { if (format == TextureInputFormat::Unknown) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateTextureInputFormat", "Invalid texture input format")); return false; } return true; } Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType) { if (texturePixelDataType == TexturePixelDataType::Unknown) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateTexturePixelDataType", "Invalid texture pixel data type")); return false; } return true; } Bool ValidateTextureLevelNumber(GLint level) { if (level < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureLevelNumber", "Texture level must be non-negative")); return false; } Int maxTextureSize = MG_Backend::DynamicBackendParameters{}.MaxTextureSize; if (MG_Backend::pActiveBackendObject) { maxTextureSize = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureSize; } Int maxLevel = 0; for (Int size = std::max(maxTextureSize, 1); size > 1; size >>= 1) { ++maxLevel; } if (level > maxLevel) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureLevelNumber", "Texture level exceeds GL_MAX_TEXTURE_SIZE")); return false; } return true; } Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) { if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX || target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY || target == TextureUploadTarget::CubeMapPositiveZ || target == TextureUploadTarget::CubeMapNegativeZ) { if (width != height) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSizeWithTarget", "Width and height must be equal for cube map textures")); return false; } } if (!(target == TextureUploadTarget::Texture1DArray || target == TextureUploadTarget::ProxyTexture1DArray)) { if (height < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSizeWithTarget", "Height must be greater than or equal to zero")); return false; } // TODO: GL_INVALID_VALUE is generated if target is not GL_TEXTURE_1D_ARRAY or GL_PROXY_TEXTURE_1D_ARRAY // and height is greater than GL_MAX_TEXTURE_SIZE. } if (target == TextureUploadTarget::Texture1DArray || target == TextureUploadTarget::ProxyTexture1DArray) { if (height < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSizeWithTarget", "Height must be greater than or equal to zero")); return false; } // TODO: GL_INVALID_VALUE is generated if target is GL_TEXTURE_1D_ARRAY or GL_PROXY_TEXTURE_1D_ARRAY and // height is greater than GL_MAX_ARRAY_TEXTURE_LAYERS. } return true; } Bool ValidateTextureSizeRange(Int width, Int height, Int depth) { if (width < 0 || height < 0 || depth < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSizeRange", "Width and height must be greater than zero")); return false; } // TODO: GL_INVALID_VALUE is generated if width is greater than GL_MAX_TEXTURE_SIZE. return true; } Bool ValidateTextureInternalFormat(TextureInternalFormat format) { if (format == TextureInternalFormat::Unknown) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormat", "Invalid texture sized internal format")); return false; } return true; } Bool ValidateTextureBorderNumber(Int border) { if (border != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureBorderNumber", "Border must be zero")); return false; } return true; } Bool IsIntegerColorInputFormat(TextureInputFormat format) { return format == TextureInputFormat::RInteger || format == TextureInputFormat::RGInteger || format == TextureInputFormat::RGBInteger || format == TextureInputFormat::BGRInteger || format == TextureInputFormat::RGBAInteger || format == TextureInputFormat::BGRAInteger || format == TextureInputFormat::GreenInteger || format == TextureInputFormat::BlueInteger || format == TextureInputFormat::AlphaInteger; } Bool IsIntegerColorInternalFormat(TextureInternalFormat internalFormat) { switch (internalFormat) { case TextureInternalFormat::R8I: case TextureInternalFormat::R8UI: case TextureInternalFormat::R16I: case TextureInternalFormat::R16UI: case TextureInternalFormat::R32I: case TextureInternalFormat::R32UI: case TextureInternalFormat::RG8I: case TextureInternalFormat::RG8UI: case TextureInternalFormat::RG16I: case TextureInternalFormat::RG16UI: case TextureInternalFormat::RG32I: case TextureInternalFormat::RG32UI: case TextureInternalFormat::RGB8I: case TextureInternalFormat::RGB8UI: case TextureInternalFormat::RGB16I: case TextureInternalFormat::RGB16UI: case TextureInternalFormat::RGB32I: case TextureInternalFormat::RGB32UI: case TextureInternalFormat::RGBA8I: case TextureInternalFormat::RGBA8UI: case TextureInternalFormat::RGBA16I: case TextureInternalFormat::RGBA16UI: case TextureInternalFormat::RGBA32I: case TextureInternalFormat::RGBA32UI: case TextureInternalFormat::RGB10A2UI: return true; default: return false; } } static Bool IsDepthLikeInternalFormat(TextureInternalFormat internalFormat) { switch (internalFormat) { case TextureInternalFormat::DepthComponent: case TextureInternalFormat::DepthComponent16: case TextureInternalFormat::DepthComponent24: case TextureInternalFormat::DepthComponent32: // not core, kept for Minecraft 1.21.5+ case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::Depth24Stencil8: case TextureInternalFormat::Depth32FStencil8: case TextureInternalFormat::DepthStencil: // Stencil-only is not a colour format either: a colour client format read against a // STENCIL_INDEX8 texture has to be the same INVALID_OPERATION as against a depth one. case TextureInternalFormat::StencilIndex8: return true; default: return false; } } static Bool IsDepthLikeInputFormat(TextureInputFormat format) { return format == TextureInputFormat::DepthComponent || format == TextureInputFormat::DepthStencil || format == TextureInputFormat::StencilIndex; } // Client-memory format<->type pairing rules shared by pixel uploads (TexImage*) and readbacks // (ReadPixels, GetTexImage). Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels // (glcPackedPixelsTests isFormatValid): packed types constrain the formats they may pair with, and // integer formats reject floating-point types; violations raise GL_INVALID_OPERATION. Bool ValidateClientFormatTypePairing(TextureInputFormat format, TexturePixelDataType type) { const auto recordInvalidOperation = [](const char* message) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", "ValidateClientFormatTypePairing", message)); return false; }; if (type == TexturePixelDataType::UnsignedByte332 || type == TexturePixelDataType::UnsignedByte233Rev || type == TexturePixelDataType::UnsignedShort565 || type == TexturePixelDataType::UnsignedShort565Rev) { if (format != TextureInputFormat::RGB && format != TextureInputFormat::RGBInteger) { return recordInvalidOperation("Packed RGB type requires RGB or RGB_INTEGER format"); } } if (type == TexturePixelDataType::UnsignedInt101111Rev || type == TexturePixelDataType::UnsignedInt5999Rev) { if (format != TextureInputFormat::RGB) { return recordInvalidOperation("Packed float RGB type requires RGB format"); } } if (type == TexturePixelDataType::UnsignedShort4444 || type == TexturePixelDataType::UnsignedShort4444Rev || type == TexturePixelDataType::UnsignedShort5551 || type == TexturePixelDataType::UnsignedShort1555Rev || type == TexturePixelDataType::UnsignedInt8888 || type == TexturePixelDataType::UnsignedInt8888Rev || type == TexturePixelDataType::UnsignedInt1010102 || type == TexturePixelDataType::UnsignedInt2101010Rev) { if (format != TextureInputFormat::RGBA && format != TextureInputFormat::BGRA && format != TextureInputFormat::RGBAInteger && format != TextureInputFormat::BGRAInteger) { return recordInvalidOperation("Packed RGBA type requires RGBA/BGRA (integer) format"); } } if (type == TexturePixelDataType::UnsignedInt248 || type == TexturePixelDataType::Float32UnsignedInt248Rev) { if (format != TextureInputFormat::DepthStencil) { return recordInvalidOperation("Packed depth-stencil type requires DEPTH_STENCIL format"); } } if (format == TextureInputFormat::DepthStencil && type != TexturePixelDataType::UnsignedInt248 && type != TexturePixelDataType::Float32UnsignedInt248Rev) { return recordInvalidOperation("DEPTH_STENCIL format requires a packed depth-stencil type"); } if (IsIntegerColorInputFormat(format) && (type == TexturePixelDataType::Float || type == TexturePixelDataType::HalfFloat)) { return recordInvalidOperation("Integer format cannot be used with a floating-point type"); } return true; } // Mirrors the desktop-GL validity matrix used by GL CTS packed_pixels (glcPackedPixelsTests // isFormatValid, INPUT_TEXIMAGE): packed-type/format pairing, depth-vs-color mismatch, and // integer-ness matching all raise GL_INVALID_OPERATION instead of reaching the upload path. Bool ValidateTextureInternalFormatCompatibleWithInput(TextureInputFormat format, TextureInternalFormat internalFormat, TexturePixelDataType type) { const auto recordInvalidOperation = [](const char* message) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", "ValidateTextureInternalFormatCompatibleWithInput", message)); return false; }; if (!ValidateClientFormatTypePairing(format, type)) { return false; } // TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). if (format == TextureInputFormat::StencilIndex) { return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); } if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) { return recordInvalidOperation("Depth/stencil-ness of format and internal format must match"); } if (IsIntegerColorInputFormat(format) != IsIntegerColorInternalFormat(internalFormat)) { return recordInvalidOperation("Integer-ness of format and internal format must match"); } return true; } Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level) { if (target == TextureUploadTarget::TextureRectangle || target == TextureUploadTarget::ProxyTextureRectangle) { if (level != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureLevelWithUploadTarget", "Level must be zero for rectangle textures")); return false; } } if (target == TextureUploadTarget::Texture2DMultisample || target == TextureUploadTarget::ProxyTexture2DMultisample || target == TextureUploadTarget::Texture2DMultisampleArray || target == TextureUploadTarget::ProxyTexture2DMultisampleArray) { if (level != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureLevelWithUploadTarget", "Level must be zero for multisample textures")); return false; } } return true; } Bool ValidateTextureLevelExists(const SharedPtr& textureObject, Int level, const char* caller) { // A null object is somebody else's error to report - ValidateTextureObject runs // first at every call site and has already recorded it. if (!textureObject) return false; const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get()); if (mipmapTexture == nullptr) { // The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is // not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target // set). Declining here is not the error code the spec asks for - that would be // INVALID_ENUM from a target check this validator is not - but it does keep a // texture with no image levels whatsoever from reaching a backend that would // dereference a backend texture it never created. MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, "Texture has no mipmap levels to address.")); return false; } // What this number is, exactly, because two other things are almost it and neither is // safe to assume: it is the number of level SLOTS the shadow has allocated - holes // included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap. // For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union. // // The guarantee that matters is one-sided: this count is always >= the level count the // backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level // with a non-positive extent, so it can only be shorter). That is the safe direction - // no copy to a level the texture genuinely has is ever rejected here. It is NOT an // exact match, so the backends keep their own range guard for the band in between: a // chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and // declined by the backend, which is a silent no-op rather than a copy. That band is a // backend storage limitation, not a validation one - rejecting it here with // INVALID_VALUE would be refusing a copy the spec permits. const Uint levelCount = mipmapTexture->GetMipmapLevelCount(); if (levelCount == 0) { // No image has ever been defined on this texture, so the fault is the texture, // not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a // copy names is an incomplete texture. MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, "Texture has no image defined at any level.")); return false; } if (level < 0 || static_cast(level) >= levelCount) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", caller, "Texture level does not exist in this texture.")); return false; } return true; } Bool ValidateTextureObject(const SharedPtr& textureObject) { if (!textureObject) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", "ValidateTextureObject", "Texture object is null")); return false; } return true; } Bool ValidateTextureNotDefault(const SharedPtr& textureObject, const char* caller) { if (textureObject && textureObject->GetExternalIndex() == 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, "This operation is not allowed on the default texture (zero is " "bound to the target).")); return false; } return true; } Bool ValidateTextureTargetUniformity(const SharedPtr& textureObject, TextureTarget target) { if (!textureObject) return true; // should be created later TextureTarget prevTarget = textureObject->GetTarget(); if (prevTarget != target) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", "ValidateTextureTargetUniformity", "Texture target does not match the previously created texture")); return false; } return true; } Bool ValidateTextureSubImageOffsets(const SharedPtr& textureObject, Int xoffset, Int width, Int yoffset, Int height, Int zoffset, Int depth) { auto baseSize = textureObject->GetBaseSize(); if (xoffset < 0 || (xoffset + width) > baseSize.x()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSubImageOffsets", "xoffset must be non-negative and (xoffset + width) must not exceed " "the texture width.")); return false; } if (baseSize.y() == 0) return true; if (yoffset < 0 || (yoffset + height) > baseSize.y()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSubImageOffsets", "yoffset must be non-negative and (yoffset + height) must not exceed " "the texture height.")); return false; } if (baseSize.z() == 0) return true; if (zoffset < 0 || (zoffset + depth) > baseSize.z()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", "ValidateTextureSubImageOffsets", "zoffset must be non-negative and (zoffset + depth) must not exceed " "the texture depth.")); return false; } return true; } namespace { // Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6 // reasons about. Colour components are independent bits so "subset" is a plain // mask test; depth and stencil are their own components and never satisfy a // colour request (or each other). enum : Uint32 { kComponentR = 1u << 0, kComponentG = 1u << 1, kComponentB = 1u << 2, kComponentA = 1u << 3, kComponentDepth = 1u << 4, kComponentStencil = 1u << 5, }; Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) { switch (unsizedFormat) { case TextureInternalFormat::Red: return kComponentR; case TextureInternalFormat::RG: return kComponentR | kComponentG; case TextureInternalFormat::RGB: return kComponentR | kComponentG | kComponentB; case TextureInternalFormat::RGBA: return kComponentR | kComponentG | kComponentB | kComponentA; case TextureInternalFormat::DepthComponent: return kComponentDepth; case TextureInternalFormat::DepthStencil: return kComponentDepth | kComponentStencil; default: return 0; } } } // namespace CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) { CopyImageTexelBlock block{}; if (compressedFormat != GL_NONE) { const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat); if (info.blockByteSize != 0) { block.byteSize = info.blockByteSize; block.blockWidth = info.blockWidth; block.blockHeight = info.blockHeight; block.compressed = true; return block; } } // The size MobileGL actually stores a texel of this format in, which for every format GL // gives a required size is that required size. The handful of legacy formats GL leaves // implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table // 8.22 to be compared against anyway, and this is the size that decides whether a raw // copy between them would in fact preserve the bytes. block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format); return block; } Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock, const CopyImageTexelBlock& dstBlock) { if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility", "A copied image has no storage whose texel size is known.")); return false; } if (srcBlock.byteSize != dstBlock.byteSize) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility", std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the " "formats are not copy-compatible.", srcBlock.byteSize, dstBlock.byteSize))); return false; } // Two compressed images additionally have to agree on the SHAPE of the block, not only // its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel // counts, and GL 4.6 core 18.3.2 requires both dimensions to match. if (srcBlock.compressed && dstBlock.compressed && (srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility", std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).", srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth, dstBlock.blockHeight))); return false; } return true; } Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height, Int imageWidth, Int imageHeight, const char* endpointName) { if (!block.compressed) return true; const Int blockWidth = static_cast(block.blockWidth); const Int blockHeight = static_cast(block.blockHeight); if (blockWidth <= 1 && blockHeight <= 1) return true; // The origin is unconditional; the extent gets the "or it reaches the edge of the image" // exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied // whole even when the last block is partial. const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0); const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth); const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight); if (originAligned && widthOk && heightOk) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique( "MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment", std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block " "grid of a {} x {} image.", endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight))); return false; } Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) { const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat); const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat); // GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components, // not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what // Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate // above and turn its rejection into an uncaught exception through the C GL ABI, so the // app died rather than seeing a GL error. const Uint32 destComponents = BaseFormatComponents(unsizedDest); const Uint32 srcComponents = BaseFormatComponents(unsizedSrc); if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset", std::format("the read buffer's base internal format {} does not provide every component of " "the requested internal format {}", MG_Util::ConvertTextureInternalFormatToString(unsizedSrc), MG_Util::ConvertTextureInternalFormatToString(unsizedDest)))); return false; } return true; } } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl