Files
MobileGL/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
T

623 lines
32 KiB
C++

// 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 <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureTarget(TextureTarget target) {
if (target == TextureTarget::Unknown) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureName", "Texture name cannot be zero"));
return false;
}
if (!MG_State::pGLContext->ValidateTextureName(texture)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureLevelWithUploadTarget",
"Level must be zero for multisample textures"));
return false;
}
}
return true;
}
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& 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<GenericErrorInfo>("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<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no image defined at any level."));
return false;
}
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level does not exist in this texture."));
return false;
}
return true;
}
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
if (!textureObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureObject", "Texture object is null"));
return false;
}
return true;
}
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
const char* caller) {
if (textureObject && textureObject->GetExternalIndex() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("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<MG_State::GLState::ITextureObject>& 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<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateTextureTargetUniformity",
"Texture target does not match the previously created texture"));
return false;
}
return true;
}
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& 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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>("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<GenericErrorInfo>(
"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<GenericErrorInfo>(
"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<Int>(block.blockWidth);
const Int blockHeight = static_cast<Int>(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<GenericErrorInfo>(
"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<GenericErrorInfo>(
"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