From 2b3850b76b9a3e9e674743b39cc867818e1c9494 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 21 Jul 2026 04:30:46 -0400 Subject: [PATCH] [Fix] (DirectGLES): upload RGB565/RGB5_A1 shadow data as packed 16-bit types - The 8-bit unorm shadow was uploaded as GL_UNSIGNED_BYTE, leaving the 8->5/6-bit requantization to the driver - That rounding direction is implementation-defined: Adreno rounds to nearest (lossless round trip), Mali floors - On Mali mid-range texels drifted one 5-bit step down, failing all 20 KHR-GL33.pixelstoragemodes.teximage3d rgb565/rgb5a1 cases (eps 1/32); layers with exact values (0.125/0.25/1.0-ish) passed, matching the observed 0,1,7-valid pattern - PreparePackedNormUpload repacks shadow rows to GL_UNSIGNED_SHORT_5_6_5 / 5_5_5_1 with round-to-nearest, which exactly recovers the original 5/6-bit values (the shadow expansion is injective), so the driver stores them verbatim - Idempotent across each region's level loop (glType is shared); RGBA4 exempt since its 8-bit expansion (v*17) is exact under either rounding - Wired at all four SyncMipmapsToBackend upload regions (append-mipmaps, immutable TexSubImage, mutable full, dirty-level update) --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index be4bdd03..893ef445 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1563,6 +1563,56 @@ namespace MobileGL::MG_Backend::DirectGLES { return convertedData.data(); } + // RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE + // leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is + // implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors, + // drifting mid-range texels one 5-bit step down and failing the KHR-GL3x + // pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into + // the packed 16-bit client type with round-to-nearest instead - that recovers the original + // 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the + // driver stores them verbatim with no requantization left to its discretion. 4-bit formats + // (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding. + // Always retargets *inOutType for these formats (even for null data) so every upload of a + // level uses the same client type. + static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize, + const void* data, SizeT byteSize, GLenum* inOutType, + Vector& packedData) { + if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) { + return data; + } + const Bool hasAlpha = format == TextureInternalFormat::RGB5A1; + const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5; + // Idempotent across a region's level loop: glType is shared, so later levels arrive with + // the already-retargeted packed type and must still be converted. + if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) { + return data; + } + *inOutType = packedType; + if (data == nullptr || byteSize == 0) { + return data; + } + const SizeT srcPixelBytes = hasAlpha ? 4 : 3; + const SizeT texelCount = std::min(static_cast(std::max(texelSize.x(), 0)) * + static_cast(std::max(texelSize.y(), 0)) * + static_cast(std::max(texelSize.z(), 1)), + byteSize / srcPixelBytes); + packedData.resize(texelCount * sizeof(Uint16)); + const Uint8* src = static_cast(data); + auto* dst = reinterpret_cast(packedData.data()); + for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) { + const Uint32 r = (static_cast(src[0]) * 31u + 127u) / 255u; + const Uint32 b = (static_cast(src[2]) * 31u + 127u) / 255u; + if (hasAlpha) { + const Uint32 g = (static_cast(src[1]) * 31u + 127u) / 255u; + dst[i] = static_cast((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u)); + } else { + const Uint32 g = (static_cast(src[1]) * 63u + 127u) / 255u; + dst[i] = static_cast((r << 11) | (g << 5) | b); + } + } + return packedData.data(); + } + void BackendTextureObject::SyncMipmapsToBackend( const SharedPtr& stateTextureObject) { if (!stateTextureObject) { @@ -1706,6 +1756,9 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, convertedUploadData); + Vector packedUploadData; + uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); @@ -1831,6 +1884,10 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, convertedUploadData); + Vector packedUploadData; + uploadData = + PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); DebugImpl::ErrorLopper::Clear(); g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); @@ -1885,6 +1942,10 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType, convertedUploadData); + Vector packedUploadData; + uploadData = + PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, + uploadData, levelByteSize, &glType, packedUploadData); MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, " "levelDirty = %s", __func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), @@ -1990,6 +2051,9 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* uploadData = PrepareNormFloatFallbackUpload( textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType, convertedUploadData); + Vector packedUploadData; + uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize, + uploadData, byteSize, &glType, packedUploadData); const IntVec3 uploadSize = GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize); switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {