diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 74181753..36134081 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -339,6 +339,11 @@ namespace MobileGL::MG_Impl::GLImpl { return GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false); case GL_TEXTURE_DEPTH_TYPE: return GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false); + case GL_TEXTURE_SHARED_SIZE: + // GL 4.6 core table 8.24: the size in bits of the SHARED EXPONENT, which only the + // one shared-exponent format has. Everything else answers zero, and the + // conformance suite compares "at least", not "equal". + return textureInternalFormat == TextureInternalFormat::RGB9E5 ? 5 : 0; default: MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname); return 0; @@ -2214,6 +2219,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureLevelNumber(level)) return; if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return; + if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return; if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; if (!TextureImpl::ValidateTextureBorderNumber(border)) return; @@ -3307,6 +3313,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_ALPHA_SIZE: case GL_TEXTURE_DEPTH_SIZE: case GL_TEXTURE_STENCIL_SIZE: + case GL_TEXTURE_SHARED_SIZE: if (params) { *params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname); } @@ -3479,6 +3486,7 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_TEXTURE_ALPHA_SIZE: case GL_TEXTURE_DEPTH_SIZE: case GL_TEXTURE_STENCIL_SIZE: + case GL_TEXTURE_SHARED_SIZE: if (params) { *params = static_cast(GetTextureLevelComponentParameter(textureObject->GetFormat(), pname)); } @@ -3644,9 +3652,54 @@ namespace MobileGL::MG_Impl::GLImpl { } } + Bool ValidateCopyTextureSubImage(const SharedPtr& textureObject, GLint level, + GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, + GLsizei depth, const char* caller); + + // The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has + // resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between + // the two forms: the DSA form takes a cube map and selects the face with zoffset (GL 4.6 core + // 8.6), while the target-taking form cannot even name a cube map here - GL_TEXTURE_CUBE_MAP is + // not in glCopyTexSubImage3D's accepted-target list, its faces go through + // glCopyTexSubImage2D - so for it zoffset is always a layer index. + static void CopyTextureSubImage3DResolved(const SharedPtr& textureObject, + GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, + GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset, + const char* caller) { + if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, caller)) { + return; + } + TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject); + GLint sliceOffset = zoffset; + if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) { + uploadTarget = static_cast( + static_cast(TextureUploadTarget::CubeMapPositiveX) + static_cast(zoffset)); + sliceOffset = 0; + } + CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y, + width, height, caller); + } + void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) { - // TODO: implement + // GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts + // exactly TEXTURE_3D, TEXTURE_2D_ARRAY and TEXTURE_CUBE_MAP_ARRAY. A cube map's faces are + // two-dimensional targets of their own and go through glCopyTexSubImage2D. + const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + if (textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::Texture2DArray && + textureTarget != TextureTarget::TextureCubeMapArray) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "glCopyTexSubImage3D requires GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY or " + "GL_TEXTURE_CUBE_MAP_ARRAY.")); + return; + } + const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); + auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + if (!textureObject) return; + CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height, + /*allowCubeFaceFromZOffset=*/false, __func__); } // What the three CopyTextureSubImage forms check in common (GL 4.6 core 8.6), once the caller @@ -4012,7 +4065,22 @@ namespace MobileGL::MG_Impl::GLImpl { } void CopyTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { - // TODO: implement + // The bound-texture form of glCopyTextureSubImage1D. GL 4.6 core 8.6 accepts only + // GL_TEXTURE_1D here. + const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); + if (textureTarget != TextureTarget::Texture1D) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "glCopyTexSubImage1D requires GL_TEXTURE_1D.")); + return; + } + const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); + auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget); + if (!textureObject) return; + if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return; + CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset, + /*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__); } Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, @@ -4468,6 +4536,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureLevelNumber(level)) return; if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return; + if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return; if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return; if (!TextureImpl::ValidateTextureBorderNumber(border)) return; if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return; @@ -5206,17 +5275,11 @@ namespace MobileGL::MG_Impl::GLImpl { return; } if (!ValidateTextureMutable(textureObject, __func__)) return; - if (textureObject->GetTarget() == TextureTarget::TextureCubeMapArray && - (width != height || depth % 6 != 0)) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique( - "MG_Impl/GLImpl", __func__, - "Cube map array immutable storage must be square with depth multiple of 6.")); - return; - } - const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject); + // The cube-array shape rules, shared with glTexImage3D / glCompressedTexImage3D so the + // three cannot drift (they had: this check used to exist here and nowhere else). + if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return; + if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; auto* textureMipmapObject = static_cast(textureObject.get()); @@ -6649,20 +6712,10 @@ namespace MobileGL::MG_Impl::GLImpl { "cube map array texture.")); return; } - if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, __func__)) { - return; - } // A cube map addresses its faces as separate upload targets, so zoffset selects the target // rather than a slice within one; every other layered target keeps zoffset as the slice. - TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject); - GLint sliceOffset = zoffset; - if (target == TextureTarget::TextureCubeMap) { - uploadTarget = static_cast( - static_cast(TextureUploadTarget::CubeMapPositiveX) + static_cast(zoffset)); - sliceOffset = 0; - } - CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y, - width, height, __func__); + CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height, + /*allowCubeFaceFromZOffset=*/true, __func__); } void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index cc85f197..e8acf306 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } + Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth, + const char* caller) { + if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) { + return true; + } + if (width != height) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, + "Cube map array levels must be square (width == height)")); + return false; + } + if (depth % 6 != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, + "Cube map array depth must be a multiple of six")); + 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 || diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 033f8fea..ef2fe37f 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType); Bool ValidateTextureLevelNumber(Int level); Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height); + // The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and + // its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in + // glTexStorage3D only, which is why glTexImage3D let both violations through - every entry + // point that DEFINES a cube-array level calls this now, so the two cannot drift again. A + // non-cube-array upload target answers true untouched. + Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth, + const char* caller); Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureBorderNumber(Int border); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 3bbbac41..995c1f2f 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -5545,3 +5545,155 @@ TEST_F(TextureTest, ImageWidenedUploadSplitsAPacked2101010RevShadowIntoFourChann EXPECT_TRUE(empty.empty()); } } + +// --------------------------------------------------------------------------------------------- +// GL_TEXTURE_CUBE_MAP_ARRAY: the shape rules, the shared-exponent level query, and the +// three-dimensional bound-texture copy. All three were front-end gaps rather than backend ones - +// the DirectVulkan baseline failed the identical conformance bodies. +// --------------------------------------------------------------------------------------------- + +// glTexStorage3D carried the two cube-array shape rules inline and glTexImage3D carried neither, +// which is exactly why esextcTextureCubeMapArrayTex3DValidation failed on its two glTexImage3D +// assertions and passed both glTexStorage3D ones. The predicate now lives in one validator that +// every level-defining entry point calls. +TEST_F(TextureTest, TexImage3DAppliesTheCubeMapArrayShapeRules) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture); + DrainPendingGlErrors(); + + // Non-square faces are GL_INVALID_VALUE. + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 4, 8, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + ExpectSingleGlError(GL_INVALID_VALUE); + + // A depth that is not a whole number of cubes is GL_INVALID_VALUE. + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 4, 4, 5, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + ExpectSingleGlError(GL_INVALID_VALUE); + + // The legal shape still goes through untouched. + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 4, 4, 12, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // And the rules are not applied to targets they do not belong to: a 2D array may be any + // rectangle with any layer count. + GLuint arrayTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &arrayTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 4, 8, 5, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0); +} + +TEST_F(TextureTest, TexStorage3DKeepsTheCubeMapArrayShapeRulesAfterTheyMovedIntoTheSharedValidator) { + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP_ARRAY, 1, &texture); + DrainPendingGlErrors(); + + MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 8, 6); + ExpectSingleGlError(GL_INVALID_VALUE); + + GLuint second = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP_ARRAY, 1, &second); + MG_Impl::GLImpl::TextureStorage3D(second, 1, GL_RGBA8, 4, 4, 5); + ExpectSingleGlError(GL_INVALID_VALUE); + + GLuint third = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP_ARRAY, 1, &third); + MG_Impl::GLImpl::TextureStorage3D(third, 1, GL_RGBA8, 4, 4, 6); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// GL_TEXTURE_SHARED_SIZE (0x8C3F) had no case in either glGetTexLevelParameter switch, so it fell +// into the terminal default arm and raised GL_INVALID_ENUM. esextcTextureCubeMapArrayGetterCalls +// walks a fixed pname list and TCU_FAILs on the first error, so the whole body died there even +// though every other pname it asks for was already implemented. +TEST_F(TextureTest, GetTexLevelParameterAnswersSharedSize) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + DrainPendingGlErrors(); + + GLint sharedSize = -1; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_SHARED_SIZE, &sharedSize); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(sharedSize, 0) << "only a shared-exponent format has a shared exponent"; + + GLfloat sharedSizeF = -1.0f; + MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_2D, 0, GL_TEXTURE_SHARED_SIZE, &sharedSizeF); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FLOAT_EQ(sharedSizeF, 0.0f) << "the fv switch is a copy of the iv one and must not drift"; + + // RGB9_E5 is the one format that HAS one, and it is five bits wide. + GLuint sharedTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &sharedTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, sharedTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB9_E5, 4, 4, 0, GL_RGB, GL_FLOAT, nullptr); + DrainPendingGlErrors(); + + sharedSize = -1; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_SHARED_SIZE, &sharedSize); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(sharedSize, 5); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +// glCopyTexSubImage3D was `{ // TODO: implement }` - no validation, no error, no copy - while its +// DSA sibling was fully implemented right next door. The two now share one body, so the target +// rules are the only thing that separates them. +TEST_F(TextureTest, CopyTexSubImage3DRejectsTargetsTheThreeDimensionalFormDoesNotTake) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + DrainPendingGlErrors(); + + // GL 4.6 core 8.6: the 3D form takes only TEXTURE_3D / TEXTURE_2D_ARRAY / + // TEXTURE_CUBE_MAP_ARRAY. A cube map's faces are two-dimensional targets and go through + // glCopyTexSubImage2D. This used to be accepted silently, which is how the defect hid. + MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, 0, 2, 2); + ExpectSingleGlError(GL_INVALID_ENUM); + MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_CUBE_MAP, 0, 0, 0, 0, 0, 0, 2, 2); + ExpectSingleGlError(GL_INVALID_ENUM); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +} + +TEST_F(TextureTest, CopyTexSubImage3DValidatesTheDestinationRegionOnACubeMapArray) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 4, 4, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + DrainPendingGlErrors(); + + // A negative level is GL_INVALID_VALUE, and reaching it at all proves the entry point now + // validates instead of returning silently. + MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, -1, 0, 0, 0, 0, 0, 2, 2); + ExpectSingleGlError(GL_INVALID_VALUE); + + // So is a negative extent. + MG_Impl::GLImpl::CopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, 0, 0, -2, 2); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0); +} + +TEST_F(TextureTest, CopyTexSubImage1DRejectsAnythingButTexture1D) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + DrainPendingGlErrors(); + + MG_Impl::GLImpl::CopyTexSubImage1D(GL_TEXTURE_2D, 0, 0, 0, 0, 2); + ExpectSingleGlError(GL_INVALID_ENUM); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); +}