diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index bf3aaa7d..b02257ea 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1715,6 +1715,7 @@ namespace MobileGL::MG_Backend::DirectGLES { 0, glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, static_cast(levelTexelSize.x()), static_cast(levelTexelSize.y()), @@ -1790,6 +1791,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(baseSize.y())); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexStorage3D(target, static_cast(mipmapCount), glInternalFormat, static_cast(baseSize.x()), static_cast(baseSize.y()), @@ -1835,6 +1837,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(levelTexelSize.y()), glFormat, glType, uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D( glUploadTarget, static_cast(level), 0, 0, 0, static_cast(levelTexelSize.x()), @@ -1893,7 +1896,8 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(levelTexelSize.y()), 0, glFormat, glType, uploadData); break; } - case TextureTarget::Texture3D: { + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: { g_GLESFuncs.glTexImage3D( glUploadTarget, static_cast(level), (GLint)glInternalFormat, static_cast(levelTexelSize.x()), @@ -1987,6 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES { uploadData); break; case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, static_cast(texelSize.x()), static_cast(texelSize.y()), diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index c1a82b6e..c90fdbaf 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -255,7 +255,7 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace TextureImpl { inline Bool IsSupportedTextureTarget(TextureTarget target) { if (target == TextureTarget::Texture1D || target == TextureTarget::TextureRectangle || - target == TextureTarget::Texture1DArray || target == TextureTarget::Texture2DArray) + target == TextureTarget::Texture1DArray) return false; return true; } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index d0e958ea..77f9a9e3 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -284,10 +284,16 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS)); } - Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) { + // Array targets store their layer count in z; layers never participate in mip + // reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level. + Bool DepthParticipatesInMipmapping(TextureTarget target) { + return target == TextureTarget::Texture3D; + } + + Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) { Int maxDimension = std::max( baseTexelSize.x(), - std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); + std::max(baseTexelSize.y(), depthMips ? std::max(baseTexelSize.z(), 1) : 1)); Uint mipLevelCount = 1; while (maxDimension > 1) { maxDimension = std::max(maxDimension / 2, 1); @@ -296,11 +302,12 @@ namespace MobileGL::MG_Impl::GLImpl { return mipLevelCount; } - IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) { + IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) { return { std::max(baseTexelSize.x() >> static_cast(relativeLevel), 1), std::max(baseTexelSize.y() >> static_cast(relativeLevel), 1), - std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1), + depthMips ? std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1) + : std::max(baseTexelSize.z(), 1), }; } @@ -323,9 +330,10 @@ namespace MobileGL::MG_Impl::GLImpl { } const SizeT bytesPerTexel = baseByteSize / baseTexelCount; - const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize); + const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget()); + const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips); for (Uint level = 1; level < requiredLevelCount; ++level) { - const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level); + const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips); const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * static_cast(levelTexelSize.y()) * static_cast(levelTexelSize.z()); @@ -2939,10 +2947,13 @@ namespace MobileGL::MG_Impl::GLImpl { auto* textureMipmapObject = static_cast(textureObject.get()); textureObject->SetInternalFormat(textureInternalFormat); + // Array targets keep their layer count constant across levels; only true 3D + // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). + const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); for (GLsizei level = 0; level < levels; ++level) { const GLsizei levelWidth = std::max(1, width >> level); const GLsizei levelHeight = std::max(1, height >> level); - const GLsizei levelDepth = std::max(1, depth >> level); + const GLsizei levelDepth = depthMips ? std::max(1, depth >> level) : depth; const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight, levelDepth); textureMipmapObject->AllocateStorage(textureUploadTarget, level, diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index ab950d29..3ca79ea3 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -11,11 +11,14 @@ #include "Includes.h" #include "Init.h" #include +#include #include #include #include #include #include +#include +#include #include #include @@ -935,6 +938,141 @@ TEST_F(TextureTest, BoundTexSubImage3DRejectsOutOfRangeLevel) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); } +// The GL CTS KHR-GL33.pixelstoragemodes.teximage3d cases upload GL_TEXTURE_2D_ARRAY +// textures through glTexImage3D with UNPACK_ROW_LENGTH / IMAGE_HEIGHT / SKIP_* set to +// extract a sub-cuboid; this mirrors that shape (scaled down) on the 2D-array target. +TEST_F(TextureTest, BoundTexImage3DOn2DArrayHonorsUnpackSubcuboidSelection) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture); + + // Source cuboid: 3x3 RGBA texels per image, 3 images; skip 1 image, 1 row, 1 pixel; + // upload the 2x2x2 sub-cuboid. Each source byte equals its own offset, so the stored + // shadow bytes must equal the offsets of the selected texels. + Uint8 pixels[3 * 3 * 3 * 4]; + for (SizeT i = 0; i < sizeof(pixels); ++i) { + pixels[i] = static_cast(i); + } + + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 3); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 1); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 1); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ROW_LENGTH, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_ROWS, 0); + MG_Impl::GLImpl::PixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetTarget(), TextureTarget::Texture2DArray); + auto* mipmapObject = static_cast(textureObject.get()); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(2, 2, 2)); + + const auto* stored = + static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2DArray, 0)); + ASSERT_NE(stored, nullptr); + SizeT storedIndex = 0; + for (SizeT image = 1; image <= 2; ++image) { // SKIP_IMAGES = 1 + for (SizeT row = 1; row <= 2; ++row) { // SKIP_ROWS = 1 + for (SizeT column = 1; column <= 2; ++column) { // SKIP_PIXELS = 1 + const SizeT srcOffset = image * 36 + row * 12 + column * 4; + for (SizeT b = 0; b < 4; ++b, ++storedIndex) { + EXPECT_EQ(stored[storedIndex], static_cast(srcOffset + b)) << "byte " << storedIndex; + } + } + } + } + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The shadow mip for packed sized formats keeps the client's packed bytes, so the +// canonical transfer triple must name the packed word type; the old default fallback +// (GL_UNSIGNED_BYTE) made backends read 4 bytes per texel from a 2-byte-per-texel +// shadow (KHR-GL33.pixelstoragemodes rgba4/rgb565 uploads), and GL_RGB10_A2UI got a +// non-integer GL_RGB transfer format the driver rejects outright. +TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFormats) { + using MG_Util::TextureFormatProcessor::NormalizePixelFormat; + struct { + GLenum internalFormat; + GLenum expectedFormat; + GLenum expectedType; + } cases[] = { + {GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, + {GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5}, + {GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV}, + {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1}, + {GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV}, + }; + for (const auto& c : cases) { + GLenum outInternal = 0, outFormat = 0, outType = 0; + NormalizePixelFormat(c.internalFormat, PixelFormatNormalizeOptionBit::None, &outInternal, &outFormat, + &outType); + EXPECT_EQ(outInternal, c.internalFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.internalFormat; + EXPECT_EQ(outType, c.expectedType) << "internalformat 0x" << std::hex << c.internalFormat; + } +} + +// GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip +// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with +// GL_RGB565 was rejected as an unknown internal format. +TEST_F(TextureTest, Rgb565InternalFormatRoundTripsThroughEnumConverters) { + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB565), TextureInternalFormat::RGB5); + EXPECT_EQ(MG_Util::ConvertGLEnumToTextureInternalFormat(GL_RGB5), TextureInternalFormat::RGB5); + // The ES-facing rendition of RGB5 is GL_RGB565 (desktop GL_RGB5 is not a legal sized + // internalformat on OpenGL ES backends). + EXPECT_EQ(MG_Util::ConvertTextureInternalFormatToGLEnum(TextureInternalFormat::RGB5), + static_cast(GL_RGB565)); +} + +// Regression guard: the DirectGLES backend must treat GL_TEXTURE_2D_ARRAY as a +// syncable target — it used to be skipped entirely, so 2D-array textures were never +// uploaded or bound (KHR-GL33.pixelstoragemodes.teximage3d.* failed wholesale). +TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) { + using MobileGL::MG_Backend::DirectGLES::TextureImpl::IsSupportedTextureTarget; + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2DArray)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture3D)); + EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture2D)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1D)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::Texture1DArray)); + EXPECT_FALSE(IsSupportedTextureTarget(TextureTarget::TextureRectangle)); +} + +// 2D-array textures keep their layer count constant across mip levels (GL 3.3 §3.9); +// only true 3D textures halve depth per level. +TEST_F(TextureTest, TexStorage3DOn2DArrayKeepsLayerCountAcrossLevels) { + GLuint arrayTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &arrayTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_2D_ARRAY, 3, GL_RGBA8, 8, 8, 4); + + const auto arrayObject = MG_State::pGLContext->GetTextureObject(arrayTexture); + ASSERT_NE(arrayObject, nullptr); + auto* arrayMipmapObject = static_cast(arrayObject.get()); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 1), IntVec3(4, 4, 4)); + EXPECT_EQ(arrayMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2DArray, 2), IntVec3(2, 2, 4)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Control: a real 3D texture still halves its depth per level. + GLuint volumeTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &volumeTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, volumeTexture); + MG_Impl::GLImpl::TexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 8, 8, 4); + + const auto volumeObject = MG_State::pGLContext->GetTextureObject(volumeTexture); + ASSERT_NE(volumeObject, nullptr); + auto* volumeMipmapObject = static_cast(volumeObject.get()); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 0), IntVec3(8, 8, 4)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 1), IntVec3(4, 4, 2)); + EXPECT_EQ(volumeMipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture3D, 2), IntVec3(2, 2, 1)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, NamedTextureVectorParametersAndGettersWorkWithoutBinding) { GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 42ad434f..5654b5d3 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -131,6 +131,9 @@ namespace MobileGL { case GL_RGB4: return TextureInternalFormat::RGB4; case GL_RGB5: + // GL_RGB565 (GL 4.1 / ARB_ES2_compatibility, used directly by the GL CTS) is the + // ES-facing rendition of the legacy RGB5 resolution. + case GL_RGB565: return TextureInternalFormat::RGB5; case GL_RGB8: return TextureInternalFormat::RGB8; diff --git a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp index f97b37bc..bee9ac88 100644 --- a/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToGL/TextureEnumConverter.cpp @@ -113,7 +113,10 @@ namespace MobileGL { case TextureInternalFormat::RGB4: return GL_RGB4; case TextureInternalFormat::RGB5: - return GL_RGB5; + // Emit the ES-compatible GL_RGB565 rendition: desktop GL_RGB5 is not a legal + // sized internalformat on OpenGL ES backends, GL_RGB565 is (and GL 4.1+ + // accepts it too via ARB_ES2_compatibility). + return GL_RGB565; case TextureInternalFormat::RGB8: return GL_RGB8; case TextureInternalFormat::RGB8Snorm: diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index aa5fad0f..6d5bea22 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -282,12 +282,17 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { // Color sized other case GL_RGB9_E5: case GL_R11F_G11F_B10F: + case GL_RGB565: *outFormat = GL_RGB; break; case GL_RGB10_A2: case GL_RGB5_A1: + case GL_RGBA4: *outFormat = GL_RGBA; break; + case GL_RGB10_A2UI: + *outFormat = GL_RGBA_INTEGER; + break; // Depth case GL_DEPTH_COMPONENT16: @@ -459,11 +464,23 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outType = GL_UNSIGNED_INT_10F_11F_11F_REV; break; case GL_RGB10_A2: + case GL_RGB10_A2UI: *outType = GL_UNSIGNED_INT_2_10_10_10_REV; break; case GL_RGB5_A1: *outType = GL_UNSIGNED_SHORT_5_5_5_1; break; + // The shadow mip keeps these formats' packed client bytes (legacy copy path), + // so the canonical transfer type must stay the packed word — the previous + // default (GL_UNSIGNED_BYTE) made the backend read 4 bytes per texel from a + // 2-byte-per-texel shadow (KHR-GL33.pixelstoragemodes teximage rgba4/rgb565 + // sliced/garbled uploads). + case GL_RGBA4: + *outType = GL_UNSIGNED_SHORT_4_4_4_4; + break; + case GL_RGB565: + *outType = GL_UNSIGNED_SHORT_5_6_5; + break; // Depth case GL_DEPTH_COMPONENT16: