diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index cee98ea6..a489a7de 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5563,6 +5563,56 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glMemoryBarrierByRegion(barriers); } + // One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it. + // + // The frontend hands this backend the target the APPLICATION named, and three of the + // targets core GL has do not exist in ES at all. They are not missing here either - the + // texture managers already store a 1D texture as a height-1 2D one, a 1D array as a + // height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) - + // but glCopyImageSubData was the one path that never asked for that translation and passed + // 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen, + // and with the error only asserted on (asserts are compiled out of an INFO build) the + // destination silently keeps whatever it held. + // + // The 1D-array case is not just a rename: GL addresses its layers with y/height while the + // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the + // target. + struct GLESCopyImageEndpoint { + GLenum target = GL_TEXTURE_2D; + GLint x = 0; + GLint y = 0; + GLint z = 0; + }; + + static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) { + const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); + GLESCopyImageEndpoint endpoint{}; + endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); + if (stateTarget == TextureTarget::Texture1DArray) { + endpoint.x = x; + endpoint.y = 0; + endpoint.z = y; + return endpoint; + } + endpoint.x = x; + endpoint.y = y; + endpoint.z = z; + return endpoint; + } + + // The region extent swaps the same two axes for a 1D array, and does so for whichever side + // of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers + // move, so at most one of the two can be a 1D array only in the degenerate single-layer + // case, where the swap is the identity anyway. + static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) { + const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget); + const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget); + if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) { + return; + } + std::swap(height, depth); + } + void CopyImageSubData(const SharedPtr& srcTexture, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, const SharedPtr& dstTexture, @@ -5592,6 +5642,12 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ); + const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ); + GLsizei copyHeight = srcHeight; + GLsizei copyDepth = srcDepth; + ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth); + const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat()); const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat()); const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat()); @@ -5599,12 +5655,12 @@ namespace MobileGL::MG_Backend::DirectGLES { if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) { MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil, "DirectGLES CopyImageSubData only supports depth-only image copies."); - MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D, + MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1, + MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, "DirectGLES depth CopyImageSubData only supports single-layer copies."); - BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight, - dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight); + BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); return; } @@ -5615,29 +5671,43 @@ namespace MobileGL::MG_Backend::DirectGLES { // with the always-live helper so a stale flag cannot misroute a // succeeded native copy into the 2D-only fallback. ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ, - dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ, - srcWidth, srcHeight, srcDepth); + g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, + dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, + srcWidth, copyHeight, copyDepth); const GLenum copyImageError = g_GLESFuncs.glGetError(); if (copyImageError == GL_NO_ERROR) { return; } MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()), "DirectGLES CopyImageSubData only supports color-only or depth-only copies."); - MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D, + MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1, + MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, "DirectGLES color CopyImageSubData only supports single-layer copies."); - CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight, - dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY); + CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y); return; } ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ, - dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ, - srcWidth, srcHeight, srcDepth); - AssertNoGLError("glCopyImageSubData"); + g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, + dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, + srcWidth, copyHeight, copyDepth); + // Every error condition glCopyImageSubData has was already ruled out by the frontend + // validator, so a driver error here is an internal invariant violation, not something + // the application can provoke. Say so where an INFO build can still see it, then trap + // in the builds that trap - the previous bare assert left a release build with a + // destination that silently kept its old contents. + const GLenum copyImageError = g_GLESFuncs.glGetError(); + if (copyImageError != GL_NO_ERROR) { + MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)", + MG_Util::ConvertGLEnumToString(copyImageError).c_str(), + MG_Util::ConvertGLEnumToString(src.target).c_str(), + MG_Util::ConvertGLEnumToString(srcTarget).c_str(), + MG_Util::ConvertGLEnumToString(dst.target).c_str(), + MG_Util::ConvertGLEnumToString(dstTarget).c_str()); + MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); + } } void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 138fc924..f1327ef3 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl { dstY, dstZ, srcWidth, srcHeight, srcDepth); } + namespace { + // The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE + // enums and every PROXY enum all convert to a TextureTarget this frontend recognises, + // so ValidateTextureTarget lets them through; here they are INVALID_ENUM. + Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) { + switch (target) { + case GL_RENDERBUFFER: + case GL_TEXTURE_1D: + case GL_TEXTURE_1D_ARRAY: + case GL_TEXTURE_2D: + case GL_TEXTURE_2D_ARRAY: + case GL_TEXTURE_2D_MULTISAMPLE: + case GL_TEXTURE_2D_MULTISAMPLE_ARRAY: + case GL_TEXTURE_3D: + case GL_TEXTURE_CUBE_MAP: + case GL_TEXTURE_CUBE_MAP_ARRAY: + case GL_TEXTURE_RECTANGLE: + return true; + default: + break; + } + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique( + "MG_Impl/GLImpl", "ValidateCopyImageSubData_State", + std::format("{} is not a target glCopyImageSubData accepts as the {}.", + MG_Util::ConvertGLEnumToString(target), endpointName))); + return false; + } + + IntVec3 GetCopyImageLevelSize(const SharedPtr& textureObject, + TextureUploadTarget uploadTarget, GLint level) { + const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get()); + if (!mipmapTexture) return textureObject->GetBaseSize(); + return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast(level)); + } + + // glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2 + // spells the failure INVALID_VALUE - "if either name does not correspond to a valid + // object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for + // the ~30 entry points that reach it through a BOUND object (where the name was never + // in question and the fault is the binding), so this is a local rule rather than a + // change to the helper. + Bool ValidateCopyImageObjectExists(const SharedPtr& textureObject, + const char* endpointName) { + if (textureObject) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique( + "MG_Impl/GLImpl", "ValidateCopyImageSubData_State", + std::format("The {} name does not correspond to an existing image object.", endpointName))); + return false; + } + + // Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that + // does not match the object INVALID_ENUM, where the shared uniformity helper records + // INVALID_OPERATION for the upload paths that share it. + Bool ValidateCopyImageTargetMatchesObject(const SharedPtr& textureObject, + TextureTarget target, const char* endpointName) { + if (!textureObject || textureObject->GetTarget() == target) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique( + "MG_Impl/GLImpl", "ValidateCopyImageSubData_State", + std::format("The {} target {} does not match the target the object was created with ({}).", + endpointName, MG_Util::ConvertTextureTargetToString(target), + MG_Util::ConvertTextureTargetToString(textureObject->GetTarget())))); + return false; + } + } // namespace + Bool ValidateCopyImageSubData_State(const SharedPtr& srcTexture, - GLenum srcTarget, GLint srcLevel, + GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, const SharedPtr& dstTexture, - GLenum dstTarget, GLint dstLevel, + GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) { + if (!ValidateCopyImageObjectExists(srcTexture, "source") || + !ValidateCopyImageObjectExists(dstTexture, "destination")) { return false; } const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget); @@ -3396,8 +3468,13 @@ namespace MobileGL::MG_Impl::GLImpl { !TextureImpl::ValidateTextureTarget(dstTextureTarget)) { return false; } - if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) || - !TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) { + // GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but + // 18.3.2 does not accept them here - only the eleven whole-image targets do. + if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) { + return false; + } + if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") || + !ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) { return false; } if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) || @@ -3425,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl { if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) { return false; } - if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) { + // A multisample image can only be copied to one with the same sample count, and a + // single-sample image reports zero - so this one comparison is also what rejects + // copying between a multisample target and a non-multisample one. + if (srcTexture->GetSamples() != dstTexture->GetSamples()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + std::format("The two images have different sample counts ({} vs. {}).", + srcTexture->GetSamples(), dstTexture->GetSamples()))); + return false; + } + // 18.3.2: both images must be complete. An incomplete one has no defined texels to copy + // and no defined storage to copy into. + if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", __func__, + std::format("A copied image is incomplete (source complete: {}, destination complete: {}).", + srcTexture->IsComplete(), dstTexture->IsComplete()))); + return false; + } + const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture); + const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture); + const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock( + srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel)); + const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock( + dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel)); + if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) { + return false; + } + const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel); + const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel); + if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight, + srcLevelSize.x(), srcLevelSize.y(), "source") || + !TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight, + dstLevelSize.x(), dstLevelSize.y(), "destination")) { return false; } return true; @@ -5600,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl { void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - auto srcTexture = GetTextureObjectByName(srcName, __func__); - auto dstTexture = GetTextureObjectByName(dstName, __func__); - if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel, - srcWidth, srcHeight, srcDepth)) { + // A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is + // INVALID_OPERATION - so resolve through the plain lookup, which answers a null + // SharedPtr, and let the validator record the error this entry point owes. + const SharedPtr srcTexture = + MG_State::pGLContext->GetTextureObject(srcName); + const SharedPtr dstTexture = + MG_State::pGLContext->GetTextureObject(dstName); + if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget, + dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) { return; } CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index a08ca473..1b1dd6f2 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTextureTarget(TextureTarget target) { @@ -515,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { } } // namespace - Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) { - const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1); - const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2); - if (unsizedFormat1 != unsizedFormat2) { - // The 3-argument GenericErrorInfo constructor used to be spelled as a single - // std::format() call whose format string was the component name, so every - // diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then - // hand over component/function/message separately. + 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", "ValidateBaseInternalFormatMatch", - std::format("The base internal format of the two formats do not match ({} vs. {})", - MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1), - MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2)))); + "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); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 20b74553..6ccb7782 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -50,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { TextureTarget target); Bool ValidateTextureSubImageOffsets(const SharedPtr& textureObject, Int xoffset, Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0); - // Exact base-format equality - what glCopyImageSubData's format compatibility needs. - Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2); + // The texel block of one glCopyImageSubData endpoint, resolved to the two things the + // compatibility rule actually asks about. `compressed` is not redundant with a block bigger + // than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of + // a blocked image" from "uncompressed, and so it is measured in texels". + struct CopyImageTexelBlock { + SizeT byteSize = 0; + Uint blockWidth = 1; + Uint blockHeight = 1; + Bool compressed = false; + }; + // `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or + // GL_NONE. It has to be asked for separately because MobileGL stores every compressed format + // in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat + // alone can no longer tell a BPTC image from the RGBA8 backing it. + CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat); + // GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel + // blocks are the same SIZE - not that they share a base internal format. RGBA32UI into + // RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image + // pairs with an uncompressed one whose texel is as big as the compressed block. + Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock, + const CopyImageTexelBlock& dstBlock); + // GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block + // boundary and its size must be a whole number of blocks - unless the edge it runs to is + // the edge of the image. + Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height, + Int imageWidth, Int imageHeight, const char* endpointName); // GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component // the requested internalformat asks for, but may supply more. Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 993cec8e..38b21563 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -4033,3 +4033,248 @@ TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) { EXPECT_EQ(compressed, GL_FALSE); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// ===================== glCopyImageSubData validation (KHR-GL43.copy_image) ===================== +// +// Every case below is a mechanism the conformance group caught in the field, and each one is +// pinned here because the backend cannot: a wrongly ACCEPTED copy shows up only as wrong pixels +// on a device, and a wrongly REJECTED one shows up only as a conformance failure. + +namespace { + struct CopyImageSubDataCall { + Bool Called = false; + GLenum SrcTarget = GL_NONE; + GLenum DstTarget = GL_NONE; + GLint SrcZ = -1; + GLint DstZ = -1; + GLsizei Depth = -1; + } g_copyImageSubDataCall; + + void RecordCopyImageSubData(const SharedPtr& srcTexture, GLenum srcTarget, + GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, + const SharedPtr& dstTexture, GLenum dstTarget, + GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, + GLsizei srcHeight, GLsizei srcDepth) { + (void)srcTexture; + (void)srcLevel; + (void)srcX; + (void)srcY; + (void)dstTexture; + (void)dstLevel; + (void)dstX; + (void)dstY; + (void)srcWidth; + (void)srcHeight; + g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth}; + } + + // Two storage-backed 2D textures of the requested formats, so a copy between them is a legal + // call in every respect except the one the test is about. + void MakeCopyImagePair(GLenum srcFormat, GLenum dstFormat, GLuint& srcTexture, GLuint& dstTexture, + GLsizei levels = 1, GLsizei extent = 8) { + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &srcTexture); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &dstTexture); + MG_Impl::GLImpl::TextureStorage2D(srcTexture, levels, srcFormat, extent, extent); + MG_Impl::GLImpl::TextureStorage2D(dstTexture, levels, dstFormat, extent, extent); + } +} // namespace + +// GL 4.6 core 18.3.2 compatibility is texel-block SIZE, not base internal format. RGB10_A2 and +// R11F_G11F_B10F are both 32-bit and their bases differ (RGBA vs RGB); the old exact-base-format +// predicate rejected the pair, which is what took down the whole cross-format half of the +// conformance matrix on both backends. +TEST_F(TextureTest, CopyImageSubDataAcceptsEqualTexelSizeAcrossDifferentBaseFormats) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGB10_A2, GL_R11F_G11F_B10F, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0, + 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The other half of the same rule: equal base format is not sufficient either. RGBA8 and RGBA32F +// are both RGBA and 32 vs 128 bits, so the copy is illegal. +TEST_F(TextureTest, CopyImageSubDataRejectsDifferentTexelSizesWithTheSameBaseFormat) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGBA8, GL_RGBA32F, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0, + 4, 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + +// ...and the pairing that is legal purely because the sizes agree, across integer-ness too. +TEST_F(TextureTest, CopyImageSubDataAcceptsIntegerAndFloatOfTheSameTexelSize) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGBA32UI, GL_RGBA32F, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0, + 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// 18.3.2 spells a name that is not an object INVALID_VALUE. The shared texture-object validator +// says INVALID_OPERATION, which is right for the entry points that reach an object through a +// BINDING - hence a rule local to this entry point rather than a change to the helper. +TEST_F(TextureTest, CopyImageSubDataNonExistentNameIsInvalidValue) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + MG_Impl::GLImpl::CopyImageSubData(4242, GL_TEXTURE_2D, 0, 0, 0, 0, 4243, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +// A target that disagrees with the object it names is INVALID_ENUM, not the INVALID_OPERATION the +// shared target-uniformity validator records for the upload paths. +TEST_F(TextureTest, CopyImageSubDataTargetNotMatchingTheObjectIsInvalidEnum) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY, 0, 0, + 0, 0, 1, 1, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_ENUM); +} + +// The eleven whole-image targets only: a cube FACE converts to a target the frontend knows, so the +// generic target validator lets it through, but 18.3.2 does not accept it here. +TEST_F(TextureTest, CopyImageSubDataRejectsTargetsOutsideTheSpecList) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, 0, dstTexture, + GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_ENUM); +} + +// A level the image does not have is INVALID_VALUE; a single-level texture asked for level 1 used +// to reach the backend with whatever the storage layer answered for that level. +TEST_F(TextureTest, CopyImageSubDataRejectsLevelTheImageDoesNotHave) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 1, 0, 0, 0, + 1, 1, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +// Sample counts must match. A single-sample image reports zero, so this same comparison is also +// what refuses a copy between a multisample target and a non-multisample one. +TEST_F(TextureTest, CopyImageSubDataRejectsSampleCountMismatch) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + // Two DIFFERENT counts are the whole point, so the case needs an implementation that offers + // more than one. The unit-test context has no real backend behind GL_MAX_SAMPLES. + GLint maxSamples = 1; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &maxSamples); + if (maxSamples < 2) { + GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; no two distinct sample counts to disagree about"; + } + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcTexture); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstTexture); + MG_Impl::GLImpl::TextureStorage2DMultisample(srcTexture, 1, GL_RGBA8, 8, 8, GL_FALSE); + MG_Impl::GLImpl::TextureStorage2DMultisample(dstTexture, maxSamples, GL_RGBA8, 8, 8, GL_FALSE); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstTexture, + GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 1, 1, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + +// The layer range has to survive the frontend intact. Both backends used to drop it - DirectVulkan +// pinned baseArrayLayer/layerCount at 0/1 - so a 12-layer copy moved one layer and said nothing; +// this pins the frontend half of that contract. +TEST_F(TextureTest, CopyImageSubDataForwardsTheWholeLayerRangeToTheBackend) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture); + MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12); + MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, dstTexture, GL_TEXTURE_2D_ARRAY, + 0, 0, 0, 5, 4, 4, 7); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(g_copyImageSubDataCall.SrcZ, 2); + EXPECT_EQ(g_copyImageSubDataCall.DstZ, 5); + EXPECT_EQ(g_copyImageSubDataCall.Depth, 7); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// A rectangle target reaches the backend as itself. The translation to the GL_TEXTURE_2D the ES +// driver actually stores it in belongs to DirectGLES, not here - and putting it here would break +// DirectVulkan, which needs the real target to tell an array copy from a flat one. +TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcTexture = 0; + GLuint dstTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcTexture); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstTexture); + MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8); + MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstTexture, GL_TEXTURE_RECTANGLE, + 0, 0, 0, 0, 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(g_copyImageSubDataCall.SrcTarget, static_cast(GL_TEXTURE_RECTANGLE)); + EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast(GL_TEXTURE_RECTANGLE)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +}