From 654779e8c013e5488c78a733432466d0a8d8cb2a Mon Sep 17 00:00:00 2001 From: Marlon Date: Wed, 4 Nov 2026 22:01:51 +0100 Subject: [PATCH 1/4] fix: decode ASTC on the CPU when the GPU lacks support Desktop NVIDIA and AMD drivers don't expose GL_KHR_texture_compression_astc_ldr, so creating storage with an ASTC internal format fails with GL_INVALID_ENUM. The texture ends up with no storage and every material using it renders black, which broke mobile games such as Super String (UE4.25, Android ASTC paks). ASTC levels are now decoded to RGBA8 before the upload when the driver has no ASTC support. Drivers with support still receive the blocks as-is. ASTC 8x5, 8x6 and 10x8 have no GL format mapping here, so they are always decoded. Co-Authored-By: Claude Opus 5.5 --- Snooper/Core/Containers/Textures/Texture2D.cs | 12 +++++++++++- Snooper/Core/Hardware/DeviceInfo.cs | 2 ++ Snooper/Core/Hardware/ExtensionSupport.cs | 8 ++++++++ Snooper/Extensions/TextureExtensions.cs | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Snooper/Core/Containers/Textures/Texture2D.cs b/Snooper/Core/Containers/Textures/Texture2D.cs index 0652a47..f7575e4 100644 --- a/Snooper/Core/Containers/Textures/Texture2D.cs +++ b/Snooper/Core/Containers/Textures/Texture2D.cs @@ -121,7 +121,6 @@ public override void Prepare() Width = mips[first].SizeX; Height = mips[first].SizeY; - FormatInfo = owner.Format.GetTextureFormat(owner.SRGB); for (var i = first + 1; i < mips.Length && !_terrain; i++) { @@ -131,6 +130,17 @@ public override void Prepare() levels.Add(data); } + var format = owner.Format; + if (format.IsDecodedOnCpu()) + { + for (var i = 0; i < levels.Count; i++) + { + levels[i] = format.DecodeOnCpu(levels[i], Math.Max(1, Width >> i), Math.Max(1, Height >> i)); + } + format = EPixelFormat.PF_R8G8B8A8; + } + FormatInfo = format.GetTextureFormat(owner.SRGB); + _levels = [.. levels]; byte[]? Read(int index) => mips[index].BulkData?.ReadDataOnce() is { Length: > 0 } data ? data diff --git a/Snooper/Core/Hardware/DeviceInfo.cs b/Snooper/Core/Hardware/DeviceInfo.cs index 1e174bf..2cde0f7 100644 --- a/Snooper/Core/Hardware/DeviceInfo.cs +++ b/Snooper/Core/Hardware/DeviceInfo.cs @@ -16,6 +16,7 @@ public class DeviceInfo public static bool IsIntel { get; private set; } public static bool HasFragmentBarycentric { get; private set; } + public static bool HasAstc { get; private set; } public static string GlslDefines { get; private set; } = string.Empty; public static string FragmentGlslDefines { get; private set; } = string.Empty; @@ -33,6 +34,7 @@ public void Load() IsIntel = Vendor.Contains("Intel", StringComparison.OrdinalIgnoreCase); HasFragmentBarycentric = ExtensionSupport.SupportFragmentBarycentric; + HasAstc = ExtensionSupport.SupportAstcLdr; GlslDefines = IsIntel ? "#define BINDLESS_RAW_HANDLES\n" : string.Empty; FragmentGlslDefines = HasFragmentBarycentric ? "#extension GL_NV_fragment_shader_barycentric : require\n#define WIREFRAME\n" : string.Empty; } diff --git a/Snooper/Core/Hardware/ExtensionSupport.cs b/Snooper/Core/Hardware/ExtensionSupport.cs index 3547d97..5c273ae 100644 --- a/Snooper/Core/Hardware/ExtensionSupport.cs +++ b/Snooper/Core/Hardware/ExtensionSupport.cs @@ -24,6 +24,11 @@ public class ExtensionSupport /// public bool SupportFragmentBarycentric { get; private set; } + /// + /// GL_KHR_texture_compression_astc_ldr, desktop nvidia and amd drivers don't have it + /// + public bool SupportAstcLdr { get; private set; } + public string[] Extensions { get; private set; } = []; public void Load() @@ -46,6 +51,9 @@ public void Load() case "GL_NV_fragment_shader_barycentric": SupportFragmentBarycentric = true; break; + case "GL_KHR_texture_compression_astc_ldr": + SupportAstcLdr = true; + break; } Extensions[i] = ext; diff --git a/Snooper/Extensions/TextureExtensions.cs b/Snooper/Extensions/TextureExtensions.cs index 849c215..5d7245c 100644 --- a/Snooper/Extensions/TextureExtensions.cs +++ b/Snooper/Extensions/TextureExtensions.cs @@ -2,9 +2,11 @@ using System.Numerics; using System.Runtime.InteropServices; using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse_Conversion.Textures.ASTC; using CUE4Parse_Conversion.Textures.BC; using OpenTK.Graphics.OpenGL4; using Snooper.Core.Containers.Textures; +using Snooper.Core.Hardware; using Snooper.Rendering.Cache; namespace Snooper.Extensions; @@ -111,6 +113,23 @@ bool Swizzle(ReadOnlySpan layout, out EMaskChannels absent) } } + /// + /// the gpu can't sample these blocks, they're decoded to rgba8 on the cpu before the upload + /// + public static bool IsDecodedOnCpu(this EPixelFormat format) + => format is EPixelFormat.PF_ASTC_4x4 or EPixelFormat.PF_ASTC_6x6 or EPixelFormat.PF_ASTC_8x8 or EPixelFormat.PF_ASTC_10x10 or EPixelFormat.PF_ASTC_12x12 + ? !DeviceInfo.HasAstc + : format is EPixelFormat.PF_ASTC_8x5 or EPixelFormat.PF_ASTC_8x6 or EPixelFormat.PF_ASTC_10x8; // no gl format for these, see GetCompressedFormat + + /// + /// rgba8 texels of a level whose format + /// + public static byte[] DecodeOnCpu(this EPixelFormat format, byte[] blocks, int width, int height) + { + var info = PixelFormatUtils.PixelFormats[format]; + return ASTCDecoder.RGBA8888(blocks, info.BlockSizeX, info.BlockSizeY, info.BlockSizeZ, width, height, 1); + } + public static ITextureFormatInfo GetTextureFormat(this EPixelFormat format, bool srgb) { if (format.IsCompressed()) From d0fb5dcaa78cb8b56a86daf7c09b14376b33c4e1 Mon Sep 17 00:00:00 2001 From: Asval Date: Thu, 5 Nov 2026 19:38:50 +0100 Subject: [PATCH 2/4] let c4p decode if we can't tell the format --- Editor/ImGuiFontTexture.cs | 2 +- Editor/Managers/ImGuiController.cs | 15 +- .../Containers/Textures/ITextureFormatInfo.cs | 182 ++----------- Snooper/Core/Containers/Textures/Texture.cs | 1 + Snooper/Core/Containers/Textures/Texture2D.cs | 22 +- .../Core/Containers/Textures/TextureView.cs | 3 +- Snooper/Core/Hardware/ExtensionSupport.cs | 2 +- Snooper/Extensions/TextureExtensions.cs | 252 +++--------------- 8 files changed, 92 insertions(+), 387 deletions(-) diff --git a/Editor/ImGuiFontTexture.cs b/Editor/ImGuiFontTexture.cs index b376ba6..046ba13 100644 --- a/Editor/ImGuiFontTexture.cs +++ b/Editor/ImGuiFontTexture.cs @@ -4,7 +4,7 @@ namespace Editor; -public class ImGuiFontTexture() : Texture2D(0, 0, SizedInternalFormat.Rgba32f, PixelFormat.Rgba, PixelType.UnsignedByte) +public class ImGuiFontTexture() : Texture2D(0, 0, SizedInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte) { protected override bool CanBeViewed => false; // a preview is opaque, the glyphs need their alpha diff --git a/Editor/Managers/ImGuiController.cs b/Editor/Managers/ImGuiController.cs index becc910..eb8a9b9 100644 --- a/Editor/Managers/ImGuiController.cs +++ b/Editor/Managers/ImGuiController.cs @@ -12,6 +12,7 @@ using Snooper.Core.Containers; using Snooper.Core.Containers.Buffers; using Snooper.Core.Containers.Programs; +using Snooper.Core.Containers.Resources; using Snooper.UI; using ErrorCode = OpenTK.Graphics.OpenGL4.ErrorCode; @@ -37,17 +38,11 @@ public void Load() _ebo.Allocate(45000); _vbo.Allocate(30000); - GL.VertexArrayVertexBuffer(_vao, 0, _vbo, 0, _vbo.Stride); GL.VertexArrayElementBuffer(_vao, _ebo); - GL.VertexArrayAttribFormat(_vao, 0, 2, VertexAttribType.Float, false, 0); - GL.VertexArrayAttribFormat(_vao, 1, 2, VertexAttribType.Float, false, 8); - GL.VertexArrayAttribFormat(_vao, 2, 4, VertexAttribType.UnsignedByte, true, 16); - GL.EnableVertexArrayAttrib(_vao, 0); - GL.EnableVertexArrayAttrib(_vao, 1); - GL.EnableVertexArrayAttrib(_vao, 2); - GL.VertexArrayAttribBinding(_vao, 0, 0); - GL.VertexArrayAttribBinding(_vao, 1, 0); - GL.VertexArrayAttribBinding(_vao, 2, 0); + new VertexArrayLayout(_vao, _vbo, _vbo.Stride) + .Float(0, 2) + .Float(1, 2, offset: 8) + .Float(2, 4, VertexAttribType.UnsignedByte, true, 16); _fontTexture.Generate(); diff --git a/Snooper/Core/Containers/Textures/ITextureFormatInfo.cs b/Snooper/Core/Containers/Textures/ITextureFormatInfo.cs index adaebdc..9b5cd38 100644 --- a/Snooper/Core/Containers/Textures/ITextureFormatInfo.cs +++ b/Snooper/Core/Containers/Textures/ITextureFormatInfo.cs @@ -1,30 +1,35 @@ -using OpenTK.Graphics.OpenGL4; -using Snooper.Extensions; +using CUE4Parse.UE4.Assets.Exports.Texture; +using OpenTK.Graphics.OpenGL4; namespace Snooper.Core.Containers.Textures; public interface ITextureFormatInfo { public SizedInternalFormat InternalFormat { get; } + public SizedInternalFormat LinearFormat { get; } public long GetMemorySize(int width, int height, int depth = 1); - public bool IsSrgb => InternalFormat.IsSrgb(); + public bool IsSrgb => InternalFormat != LinearFormat; } public readonly struct TextureFormatInfo : ITextureFormatInfo { public SizedInternalFormat InternalFormat { get; } + public SizedInternalFormat LinearFormat { get; } public readonly PixelFormat Format; public readonly PixelType Type; private readonly int _bytesPerPixel; + private readonly string? _name; - public TextureFormatInfo(SizedInternalFormat internalFormat, PixelFormat format, PixelType type) + public TextureFormatInfo(SizedInternalFormat internalFormat, PixelFormat format, PixelType type, SizedInternalFormat? linearFormat = null, string? name = null) { InternalFormat = internalFormat; + LinearFormat = linearFormat ?? internalFormat; Format = format; Type = type; + _name = name; _bytesPerPixel = GetBytesPerPixel(); } @@ -36,186 +41,47 @@ public long GetMemorySize(int width, int height, int depth = 1) private int GetBytesPerPixel() { - return InternalFormat switch + return LinearFormat switch { - // 8-bit formats SizedInternalFormat.R8 => 1, - SizedInternalFormat.Rg8 => 2, + SizedInternalFormat.Rg8 or SizedInternalFormat.Rg8Snorm => 2, SizedInternalFormat.Rgb8 or SizedInternalFormat.Srgb8 => 3, SizedInternalFormat.Rgba8 or SizedInternalFormat.Srgb8Alpha8 => 4, - // 16-bit formats - SizedInternalFormat.R16 => 2, - SizedInternalFormat.R16f => 2, - SizedInternalFormat.Rg16 => 4, - SizedInternalFormat.Rg16f => 4, + SizedInternalFormat.R16 or SizedInternalFormat.R16f => 2, + SizedInternalFormat.Rg16 or SizedInternalFormat.Rg16f => 4, SizedInternalFormat.Rgb16 or SizedInternalFormat.Rgb16f => 6, SizedInternalFormat.Rgba16 or SizedInternalFormat.Rgba16f => 8, - // 32-bit formats - SizedInternalFormat.R32f => 4, + SizedInternalFormat.R32f or SizedInternalFormat.R32ui => 4, SizedInternalFormat.Rg32f => 8, SizedInternalFormat.Rgb32f => 12, SizedInternalFormat.Rgba32f => 16, - // Depth and stencil formats SizedInternalFormat.DepthComponent16 => 2, SizedInternalFormat.DepthComponent24 => 3, - SizedInternalFormat.DepthComponent32f => 4, - SizedInternalFormat.Depth24Stencil8 => 4, + SizedInternalFormat.DepthComponent32f or SizedInternalFormat.Depth24Stencil8 => 4, SizedInternalFormat.Depth32fStencil8 => 5, - _ => Type switch - { - PixelType.UnsignedByte => 4, // Default RGBA8 - PixelType.Float => 16, // Default RGBA32F - PixelType.HalfFloat => 8, // Default RGBA16F - PixelType.UnsignedShort => 8, // Default RGBA16 - _ => 4 - } + _ => 0 }; } - public override string ToString() - { - var name = InternalFormat switch - { - SizedInternalFormat.R8 => "R8", - SizedInternalFormat.Rg8 => "RG8", - SizedInternalFormat.Rgb8 or SizedInternalFormat.Srgb8 => "RGB8", - SizedInternalFormat.Rgba8 or SizedInternalFormat.Srgb8Alpha8 => "RGBA8", - - SizedInternalFormat.R16 => "R16", - SizedInternalFormat.R16f => "R16F", - SizedInternalFormat.Rg16 => "RG16", - SizedInternalFormat.Rg16f => "RG16F", - SizedInternalFormat.Rgb16 => "RGB16", - SizedInternalFormat.Rgb16f => "RGB16F", - SizedInternalFormat.Rgba16 => "RGBA16", - SizedInternalFormat.Rgba16f => "RGBA16F", - - SizedInternalFormat.R32f => "R32F", - SizedInternalFormat.R32ui => "R32UI", - SizedInternalFormat.Rg32f => "RG32F", - SizedInternalFormat.Rgb32f => "RGB32F", - SizedInternalFormat.Rgba32f => "RGBA32F", - - SizedInternalFormat.DepthComponent16 => "D16", - SizedInternalFormat.DepthComponent24 => "D24", - SizedInternalFormat.DepthComponent32f => "D32F", - SizedInternalFormat.Depth24Stencil8 => "D24S8", - SizedInternalFormat.Depth32fStencil8 => "D32FS8", - - _ => InternalFormat.ToString() - }; - - return Format switch - { - PixelFormat.Bgra => $"BGRA -> {name}", - PixelFormat.Bgr => $"BGR -> {name}", - _ => name - }; - } + public override string ToString() => _name ?? InternalFormat.ToString(); } -public readonly struct CompressedTextureFormatInfo : ITextureFormatInfo +public readonly struct CompressedTextureFormatInfo(SizedInternalFormat internalFormat, SizedInternalFormat linearFormat, FPixelFormatInfo blocks) : ITextureFormatInfo { - public SizedInternalFormat InternalFormat { get; } - - private readonly int _blockSize; - private readonly int _blockWidth; - private readonly int _blockHeight; - - public CompressedTextureFormatInfo(SizedInternalFormat internalFormat) - { - InternalFormat = internalFormat; - - _blockSize = GetBlockSize(); - (_blockWidth, _blockHeight) = GetBlockDimensions(); - } + public SizedInternalFormat InternalFormat { get; } = internalFormat; + public SizedInternalFormat LinearFormat { get; } = linearFormat; public long GetMemorySize(int width, int height, int depth = 1) { - var blocksWide = (width + _blockWidth - 1) / _blockWidth; - var blocksHigh = (height + _blockHeight - 1) / _blockHeight; - - return (long)blocksWide * blocksHigh * depth * _blockSize; - } - - private int GetBlockSize() - { - return InternalFormat switch - { - SizedInternalFormat.CompressedRgbaS3tcDxt1Ext or - SizedInternalFormat.CompressedSrgbAlphaS3tcDxt1Ext or - SizedInternalFormat.CompressedRedRgtc1 => 8, - - SizedInternalFormat.CompressedRgbaS3tcDxt3Ext or - SizedInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext or - SizedInternalFormat.CompressedRgbaS3tcDxt5Ext or - SizedInternalFormat.CompressedSrgbAlphaS3tcDxt5Ext or - SizedInternalFormat.CompressedRgRgtc2 or - SizedInternalFormat.CompressedRgbBptcUnsignedFloat or - SizedInternalFormat.CompressedRgbaBptcUnorm => 16, - - _ when IsAstcFormat() => 16, - - _ when IsEtc2RgbFormat() => 8, - _ when IsEtc2RgbaFormat() => 16, - - _ => 16 - }; - } - - private (int width, int height) GetBlockDimensions() - { - return InternalFormat switch - { - SizedInternalFormat.CompressedRgbaAstc4X4 or SizedInternalFormat.CompressedSrgb8Alpha8Astc4X4 => (4, 4), - SizedInternalFormat.CompressedRgbaAstc6X6 or SizedInternalFormat.CompressedSrgb8Alpha8Astc6X6 => (6, 6), - SizedInternalFormat.CompressedRgbaAstc8X8 or SizedInternalFormat.CompressedSrgb8Alpha8Astc8X8 => (8, 8), - SizedInternalFormat.CompressedRgbaAstc10X10 or SizedInternalFormat.CompressedSrgb8Alpha8Astc10X10 => (10, 10), - SizedInternalFormat.CompressedRgbaAstc12X12 or SizedInternalFormat.CompressedSrgb8Alpha8Astc12X12 => (12, 12), + var blocksWide = (width + blocks.BlockSizeX - 1) / blocks.BlockSizeX; + var blocksHigh = (height + blocks.BlockSizeY - 1) / blocks.BlockSizeY; - _ => (4, 4) - }; - } - - private bool IsAstcFormat() - { - return InternalFormat is - SizedInternalFormat.CompressedRgbaAstc4X4 or SizedInternalFormat.CompressedSrgb8Alpha8Astc4X4 or - SizedInternalFormat.CompressedRgbaAstc6X6 or SizedInternalFormat.CompressedSrgb8Alpha8Astc6X6 or - SizedInternalFormat.CompressedRgbaAstc8X8 or SizedInternalFormat.CompressedSrgb8Alpha8Astc8X8 or - SizedInternalFormat.CompressedRgbaAstc10X10 or SizedInternalFormat.CompressedSrgb8Alpha8Astc10X10 or - SizedInternalFormat.CompressedRgbaAstc12X12 or SizedInternalFormat.CompressedSrgb8Alpha8Astc12X12; + return (long)blocksWide * blocksHigh * depth * blocks.BlockBytes; } - private bool IsEtc2RgbFormat() - { - return InternalFormat is SizedInternalFormat.CompressedRgb8Etc2 or SizedInternalFormat.CompressedSrgb8Etc2; - } - - private bool IsEtc2RgbaFormat() - { - return InternalFormat is SizedInternalFormat.CompressedRgba8Etc2Eac or SizedInternalFormat.CompressedSrgb8Alpha8Etc2Eac; - } - - public override string ToString() => InternalFormat switch - { - SizedInternalFormat.CompressedRgbaS3tcDxt1Ext or SizedInternalFormat.CompressedSrgbAlphaS3tcDxt1Ext => "BC1 RGB", - SizedInternalFormat.CompressedRgbaS3tcDxt3Ext or SizedInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext => "BC2 RGBA", - SizedInternalFormat.CompressedRgbaS3tcDxt5Ext or SizedInternalFormat.CompressedSrgbAlphaS3tcDxt5Ext => "BC3 RGBA", - SizedInternalFormat.CompressedRedRgtc1 => "BC4 R", - SizedInternalFormat.CompressedRgRgtc2 => "BC5 RG", - SizedInternalFormat.CompressedRgbBptcUnsignedFloat => "BC6H RGB", - SizedInternalFormat.CompressedRgbaBptcUnorm => "BC7 RGBA", - - SizedInternalFormat.CompressedRgb8Etc2 or SizedInternalFormat.CompressedSrgb8Etc2 => "ETC2 RGB", - SizedInternalFormat.CompressedRgba8Etc2Eac or SizedInternalFormat.CompressedSrgb8Alpha8Etc2Eac => "ETC2 RGBA", - - _ when IsAstcFormat() => $"ASTC {_blockWidth}x{_blockHeight} RGBA", - - _ => InternalFormat.ToString() - }; + public override string ToString() => blocks.Name; } diff --git a/Snooper/Core/Containers/Textures/Texture.cs b/Snooper/Core/Containers/Textures/Texture.cs index 6651caa..57efa9d 100644 --- a/Snooper/Core/Containers/Textures/Texture.cs +++ b/Snooper/Core/Containers/Textures/Texture.cs @@ -36,6 +36,7 @@ protected ITextureFormatInfo FormatInfo } public bool IsSrgb => FormatInfo.IsSrgb; + public SizedInternalFormat LinearFormat => FormatInfo.LinearFormat; public string FormatName { get; private set; } = string.Empty; diff --git a/Snooper/Core/Containers/Textures/Texture2D.cs b/Snooper/Core/Containers/Textures/Texture2D.cs index f7575e4..aa4f817 100644 --- a/Snooper/Core/Containers/Textures/Texture2D.cs +++ b/Snooper/Core/Containers/Textures/Texture2D.cs @@ -1,4 +1,5 @@ -using CUE4Parse_Conversion.Textures; +using System.Collections.Concurrent; +using CUE4Parse_Conversion.Textures; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Misc; using OpenTK.Graphics.OpenGL4; @@ -23,6 +24,8 @@ public class Texture2D(int width, int height, private (TextureMinFilter Min, TextureMagFilter Mag, TextureWrapMode WrapS, TextureWrapMode WrapT) _sampling; private byte[][]? _levels; + private static readonly ConcurrentDictionary _decodedFormats = new(); + public Texture2D(UTexture texture) : this(texture.PlatformData.SizeX, texture.PlatformData.SizeY, guid: texture.LightingGuid, name: texture.Name) { _owner = texture; @@ -98,7 +101,7 @@ public override void Prepare() Width = textureData.Width; Height = textureData.Height; - FormatInfo = textureData.PixelFormat.GetTextureFormat(owner.SRGB); + FormatInfo = textureData.PixelFormat.GetTextureFormat(owner.SRGB) ?? throw new NotSupportedException($"{textureData.PixelFormat} can't be uploaded."); _levels = [textureData.Data]; } else @@ -131,15 +134,22 @@ public override void Prepare() } var format = owner.Format; - if (format.IsDecodedOnCpu()) + if (format.GetTextureFormat(owner.SRGB) is not { } info) { + var source = PixelFormatUtils.PixelFormats[format]; for (var i = 0; i < levels.Count; i++) { - levels[i] = format.DecodeOnCpu(levels[i], Math.Max(1, Width >> i), Math.Max(1, Height >> i)); + TextureDecoder.DecodeBytes(levels[i], Math.Max(1, Width >> i), Math.Max(1, Height >> i), 1, source, owner.IsNormalMap, out var data, out format); + levels[i] = data; + } + info = format.GetTextureFormat(owner.SRGB) ?? throw new NotSupportedException($"{owner.Format} can't be decoded."); + + if (_decodedFormats.TryAdd(owner.Format, 0)) + { + Log.Information("{Format} can't be uploaded as is, decoding it to {Decoded} on the CPU", owner.Format, format); } - format = EPixelFormat.PF_R8G8B8A8; } - FormatInfo = format.GetTextureFormat(owner.SRGB); + FormatInfo = info; _levels = [.. levels]; diff --git a/Snooper/Core/Containers/Textures/TextureView.cs b/Snooper/Core/Containers/Textures/TextureView.cs index d3a230b..119d721 100644 --- a/Snooper/Core/Containers/Textures/TextureView.cs +++ b/Snooper/Core/Containers/Textures/TextureView.cs @@ -1,5 +1,4 @@ using OpenTK.Graphics.OpenGL4; -using Snooper.Extensions; namespace Snooper.Core.Containers.Textures; @@ -20,7 +19,7 @@ public override void Generate() uint original = source; GL.GetTextureLevelParameter(original, 0, GetTextureParameter.TextureInternalFormat, out int format); - if (linear) format = (int) ((SizedInternalFormat) format).ToLinear(); + if (linear) format = (int) source.LinearFormat; var handle = GL.GenTexture(); GL.TextureView(handle, source.Target, (int) original, (PixelInternalFormat) format, 0, source.MipCount, 0, 1); diff --git a/Snooper/Core/Hardware/ExtensionSupport.cs b/Snooper/Core/Hardware/ExtensionSupport.cs index 5c273ae..08243d4 100644 --- a/Snooper/Core/Hardware/ExtensionSupport.cs +++ b/Snooper/Core/Hardware/ExtensionSupport.cs @@ -25,7 +25,7 @@ public class ExtensionSupport public bool SupportFragmentBarycentric { get; private set; } /// - /// GL_KHR_texture_compression_astc_ldr, desktop nvidia and amd drivers don't have it + /// GL_KHR_texture_compression_astc_ldr /// public bool SupportAstcLdr { get; private set; } diff --git a/Snooper/Extensions/TextureExtensions.cs b/Snooper/Extensions/TextureExtensions.cs index 5d7245c..9131e33 100644 --- a/Snooper/Extensions/TextureExtensions.cs +++ b/Snooper/Extensions/TextureExtensions.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Runtime.InteropServices; using CUE4Parse.UE4.Assets.Exports.Texture; -using CUE4Parse_Conversion.Textures.ASTC; using CUE4Parse_Conversion.Textures.BC; using OpenTK.Graphics.OpenGL4; using Snooper.Core.Containers.Textures; @@ -113,33 +112,52 @@ bool Swizzle(ReadOnlySpan layout, out EMaskChannels absent) } } - /// - /// the gpu can't sample these blocks, they're decoded to rgba8 on the cpu before the upload - /// - public static bool IsDecodedOnCpu(this EPixelFormat format) - => format is EPixelFormat.PF_ASTC_4x4 or EPixelFormat.PF_ASTC_6x6 or EPixelFormat.PF_ASTC_8x8 or EPixelFormat.PF_ASTC_10x10 or EPixelFormat.PF_ASTC_12x12 - ? !DeviceInfo.HasAstc - : format is EPixelFormat.PF_ASTC_8x5 or EPixelFormat.PF_ASTC_8x6 or EPixelFormat.PF_ASTC_10x8; // no gl format for these, see GetCompressedFormat - - /// - /// rgba8 texels of a level whose format - /// - public static byte[] DecodeOnCpu(this EPixelFormat format, byte[] blocks, int width, int height) - { - var info = PixelFormatUtils.PixelFormats[format]; - return ASTCDecoder.RGBA8888(blocks, info.BlockSizeX, info.BlockSizeY, info.BlockSizeZ, width, height, 1); - } - - public static ITextureFormatInfo GetTextureFormat(this EPixelFormat format, bool srgb) + public static ITextureFormatInfo? GetTextureFormat(this EPixelFormat format, bool srgb) { - if (format.IsCompressed()) + return format switch { - var compressed = format.GetCompressedFormat(); - return new CompressedTextureFormatInfo(srgb ? compressed.ToSrgb() : compressed); - } + EPixelFormat.PF_R8G8B8A8 => Pixels(SizedInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte, SizedInternalFormat.Srgb8Alpha8), + EPixelFormat.PF_B8G8R8A8 or EPixelFormat.PF_A8R8G8B8 => Pixels(SizedInternalFormat.Rgba8, PixelFormat.Bgra, PixelType.UnsignedByte, SizedInternalFormat.Srgb8Alpha8), + EPixelFormat.PF_G8 => Pixels(SizedInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte), + EPixelFormat.PF_V8U8 => Pixels(SizedInternalFormat.Rg8Snorm, PixelFormat.Rg, PixelType.Byte), + EPixelFormat.PF_A32B32G32R32F => Pixels(SizedInternalFormat.Rgba32f, PixelFormat.Rgba, PixelType.Float), + EPixelFormat.PF_FloatRGB => Pixels(SizedInternalFormat.Rgb16f, PixelFormat.Rgb, PixelType.HalfFloat), + EPixelFormat.PF_FloatRGBA => Pixels(SizedInternalFormat.Rgba16f, PixelFormat.Rgba, PixelType.HalfFloat), + EPixelFormat.PF_R32_FLOAT => Pixels(SizedInternalFormat.R32f, PixelFormat.Red, PixelType.Float), + EPixelFormat.PF_G16R16F or EPixelFormat.PF_G16R16F_FILTER => Pixels(SizedInternalFormat.Rg16f, PixelFormat.Rg, PixelType.HalfFloat), + EPixelFormat.PF_G16R16 => Pixels(SizedInternalFormat.Rg16, PixelFormat.Rg, PixelType.UnsignedShort), + EPixelFormat.PF_G32R32F => Pixels(SizedInternalFormat.Rg32f, PixelFormat.Rg, PixelType.Float), + EPixelFormat.PF_A16B16G16R16 => Pixels(SizedInternalFormat.Rgba16, PixelFormat.Rgba, PixelType.UnsignedShort), + EPixelFormat.PF_R16F or EPixelFormat.PF_R16F_FILTER => Pixels(SizedInternalFormat.R16f, PixelFormat.Red, PixelType.HalfFloat), + EPixelFormat.PF_G16 => Pixels(SizedInternalFormat.R16, PixelFormat.Red, PixelType.UnsignedShort), + EPixelFormat.PF_R32G32B32F => Pixels(SizedInternalFormat.Rgb32f, PixelFormat.Rgb, PixelType.Float), + + EPixelFormat.PF_DXT1 => Blocks(SizedInternalFormat.CompressedRgbaS3tcDxt1Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt1Ext), + EPixelFormat.PF_DXT3 => Blocks(SizedInternalFormat.CompressedRgbaS3tcDxt3Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext), + EPixelFormat.PF_DXT5 => Blocks(SizedInternalFormat.CompressedRgbaS3tcDxt5Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt5Ext), + EPixelFormat.PF_BC4 => Blocks(SizedInternalFormat.CompressedRedRgtc1), + EPixelFormat.PF_BC5 => Blocks(SizedInternalFormat.CompressedRgRgtc2), + EPixelFormat.PF_BC6H => Blocks(SizedInternalFormat.CompressedRgbBptcUnsignedFloat), + EPixelFormat.PF_BC7 => Blocks(SizedInternalFormat.CompressedRgbaBptcUnorm, SizedInternalFormat.CompressedSrgbAlphaBptcUnorm), + + EPixelFormat.PF_ASTC_4x4 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc4X4, SizedInternalFormat.CompressedSrgb8Alpha8Astc4X4), + EPixelFormat.PF_ASTC_6x6 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc6X6, SizedInternalFormat.CompressedSrgb8Alpha8Astc6X6), + EPixelFormat.PF_ASTC_8x5 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc8X5, SizedInternalFormat.CompressedSrgb8Alpha8Astc8X5), + EPixelFormat.PF_ASTC_8x6 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc8X6, SizedInternalFormat.CompressedSrgb8Alpha8Astc8X6), + EPixelFormat.PF_ASTC_8x8 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc8X8, SizedInternalFormat.CompressedSrgb8Alpha8Astc8X8), + EPixelFormat.PF_ASTC_10x8 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc10X8, SizedInternalFormat.CompressedSrgb8Alpha8Astc10X8), + EPixelFormat.PF_ASTC_10x10 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc10X10, SizedInternalFormat.CompressedSrgb8Alpha8Astc10X10), + EPixelFormat.PF_ASTC_12x12 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc12X12, SizedInternalFormat.CompressedSrgb8Alpha8Astc12X12), + + EPixelFormat.PF_ETC1 => Blocks(SizedInternalFormat.CompressedRgb8Etc2, SizedInternalFormat.CompressedSrgb8Etc2), + EPixelFormat.PF_ETC2_RGB => Blocks(SizedInternalFormat.CompressedRgb8Etc2, SizedInternalFormat.CompressedSrgb8Etc2), + EPixelFormat.PF_ETC2_RGBA => Blocks(SizedInternalFormat.CompressedRgba8Etc2Eac, SizedInternalFormat.CompressedSrgb8Alpha8Etc2Eac), + + _ => null + }; - var (internalFormat, pixelFormat, pixelType) = format.GetUncompressedFormats(); - return new TextureFormatInfo(srgb ? internalFormat.ToSrgb() : internalFormat, pixelFormat, pixelType); + ITextureFormatInfo Pixels(SizedInternalFormat linear, PixelFormat pixelFormat, PixelType type, SizedInternalFormat? srgbFormat = null) => new TextureFormatInfo(srgb ? srgbFormat ?? linear : linear, pixelFormat, type, linear, PixelFormatUtils.PixelFormats[format].Name); + ITextureFormatInfo Blocks(SizedInternalFormat linear, SizedInternalFormat? srgbFormat = null) => new CompressedTextureFormatInfo(srgb ? srgbFormat ?? linear : linear, linear, PixelFormatUtils.PixelFormats[format]); } public static PixelInternalFormat ToPixelInternalFormat(this SizedInternalFormat format) @@ -195,190 +213,6 @@ public static InternalFormat ToInternalFormat(this SizedInternalFormat format) }; } - private static bool IsCompressed(this EPixelFormat format) - => format switch - { - EPixelFormat.PF_B8G8R8A8 or - EPixelFormat.PF_R8G8B8A8 or - EPixelFormat.PF_A8R8G8B8 or - EPixelFormat.PF_G8 or - EPixelFormat.PF_V8U8 or - EPixelFormat.PF_A32B32G32R32F or - EPixelFormat.PF_FloatRGB or - EPixelFormat.PF_FloatRGBA or - EPixelFormat.PF_R32_FLOAT or - EPixelFormat.PF_G16R16F or - EPixelFormat.PF_G16R16F_FILTER or - EPixelFormat.PF_G16R16 or - EPixelFormat.PF_G32R32F or - EPixelFormat.PF_A16B16G16R16 or - EPixelFormat.PF_R16F or - EPixelFormat.PF_R16F_FILTER or - EPixelFormat.PF_G16 or - EPixelFormat.PF_R32G32B32F => false, - _ => true - }; - - private static (SizedInternalFormat, PixelFormat, PixelType) GetUncompressedFormats(this EPixelFormat format) - { - return format switch - { - EPixelFormat.PF_B8G8R8A8 => ( - SizedInternalFormat.Rgba8, - PixelFormat.Bgra, - PixelType.UnsignedByte - ), - EPixelFormat.PF_R8G8B8A8 => ( - SizedInternalFormat.Rgba8, - PixelFormat.Rgba, - PixelType.UnsignedByte - ), - EPixelFormat.PF_A8R8G8B8 => ( - SizedInternalFormat.Rgba8, - PixelFormat.Bgra, - PixelType.UnsignedByte - ), - EPixelFormat.PF_G8 => ( - SizedInternalFormat.R8, - PixelFormat.Red, - PixelType.UnsignedByte - ), - EPixelFormat.PF_V8U8 => ( - SizedInternalFormat.Rg8Snorm, - PixelFormat.Rg, - PixelType.Byte - ), - EPixelFormat.PF_A32B32G32R32F => ( - SizedInternalFormat.Rgba32f, - PixelFormat.Rgba, - PixelType.Float - ), - EPixelFormat.PF_FloatRGB => ( - SizedInternalFormat.Rgb16f, - PixelFormat.Rgb, - PixelType.HalfFloat - ), - EPixelFormat.PF_FloatRGBA => ( - SizedInternalFormat.Rgba16f, - PixelFormat.Rgba, - PixelType.HalfFloat - ), - EPixelFormat.PF_R32_FLOAT => ( - SizedInternalFormat.R32f, - PixelFormat.Red, - PixelType.Float - ), - EPixelFormat.PF_G16R16F or EPixelFormat.PF_G16R16F_FILTER => ( - SizedInternalFormat.Rg16f, - PixelFormat.Rg, - PixelType.HalfFloat - ), - EPixelFormat.PF_G16R16 => ( - SizedInternalFormat.Rg16, - PixelFormat.Rg, - PixelType.UnsignedShort - ), - EPixelFormat.PF_G32R32F => ( - SizedInternalFormat.Rg32f, - PixelFormat.Rg, - PixelType.Float - ), - EPixelFormat.PF_A16B16G16R16 => ( - SizedInternalFormat.Rgba16, - PixelFormat.Rgba, - PixelType.UnsignedShort - ), - EPixelFormat.PF_R16F or EPixelFormat.PF_R16F_FILTER => ( - SizedInternalFormat.R16f, - PixelFormat.Red, - PixelType.HalfFloat - ), - EPixelFormat.PF_G16 => ( - SizedInternalFormat.R16, - PixelFormat.Red, - PixelType.UnsignedShort - ), - EPixelFormat.PF_R32G32B32F => ( - SizedInternalFormat.Rgb32f, - PixelFormat.Rgb, - PixelType.Float - ), - _ => throw new NotImplementedException($"Unsupported pixel format: {format}") - }; - } - - private static SizedInternalFormat GetCompressedFormat(this EPixelFormat format) - { - return format switch - { - EPixelFormat.PF_DXT1 => SizedInternalFormat.CompressedRgbaS3tcDxt1Ext, - EPixelFormat.PF_DXT3 => SizedInternalFormat.CompressedRgbaS3tcDxt3Ext, - EPixelFormat.PF_DXT5 => SizedInternalFormat.CompressedRgbaS3tcDxt5Ext, - EPixelFormat.PF_BC4 => SizedInternalFormat.CompressedRedRgtc1, - EPixelFormat.PF_BC5 => SizedInternalFormat.CompressedRgRgtc2, - EPixelFormat.PF_BC6H => SizedInternalFormat.CompressedRgbBptcUnsignedFloat, - EPixelFormat.PF_BC7 => SizedInternalFormat.CompressedRgbaBptcUnorm, - - EPixelFormat.PF_ASTC_4x4 => SizedInternalFormat.CompressedRgbaAstc4X4, - EPixelFormat.PF_ASTC_6x6 => SizedInternalFormat.CompressedRgbaAstc6X6, - EPixelFormat.PF_ASTC_8x8 => SizedInternalFormat.CompressedRgbaAstc8X8, - EPixelFormat.PF_ASTC_10x10 => SizedInternalFormat.CompressedRgbaAstc10X10, - EPixelFormat.PF_ASTC_12x12 => SizedInternalFormat.CompressedRgbaAstc12X12, - - // EPixelFormat.PF_ETC1 => SizedInternalFormat.CompressedRgb8Etc2, - EPixelFormat.PF_ETC2_RGB => SizedInternalFormat.CompressedRgb8Etc2, - EPixelFormat.PF_ETC2_RGBA => SizedInternalFormat.CompressedRgba8Etc2Eac, - - _ => throw new NotImplementedException($"Unsupported pixel format: {format}") - }; - } - - private static readonly (SizedInternalFormat Linear, SizedInternalFormat Srgb)[] _srgbFormats = - [ - (SizedInternalFormat.Rgb8, SizedInternalFormat.Srgb8), - (SizedInternalFormat.Rgba8, SizedInternalFormat.Srgb8Alpha8), - (SizedInternalFormat.CompressedRgbaS3tcDxt1Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt1Ext), - (SizedInternalFormat.CompressedRgbaS3tcDxt3Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt3Ext), - (SizedInternalFormat.CompressedRgbaS3tcDxt5Ext, SizedInternalFormat.CompressedSrgbAlphaS3tcDxt5Ext), - (SizedInternalFormat.CompressedRgbaAstc4X4, SizedInternalFormat.CompressedSrgb8Alpha8Astc4X4), - (SizedInternalFormat.CompressedRgbaAstc6X6, SizedInternalFormat.CompressedSrgb8Alpha8Astc6X6), - (SizedInternalFormat.CompressedRgbaAstc8X8, SizedInternalFormat.CompressedSrgb8Alpha8Astc8X8), - (SizedInternalFormat.CompressedRgbaAstc10X10, SizedInternalFormat.CompressedSrgb8Alpha8Astc10X10), - (SizedInternalFormat.CompressedRgbaAstc12X12, SizedInternalFormat.CompressedSrgb8Alpha8Astc12X12), - (SizedInternalFormat.CompressedRgb8Etc2, SizedInternalFormat.CompressedSrgb8Etc2), - (SizedInternalFormat.CompressedRgba8Etc2Eac, SizedInternalFormat.CompressedSrgb8Alpha8Etc2Eac), - ]; - - public static bool IsSrgb(this SizedInternalFormat format) - { - foreach (var (_, srgb) in _srgbFormats) - { - if (srgb == format) return true; - } - - return false; - } - - public static SizedInternalFormat ToSrgb(this SizedInternalFormat format) - { - foreach (var (linear, srgb) in _srgbFormats) - { - if (linear == format) return srgb; - } - - return format; - } - - public static SizedInternalFormat ToLinear(this SizedInternalFormat format) - { - foreach (var (linear, srgb) in _srgbFormats) - { - if (srgb == format) return linear; - } - - return format; - } - public static bool TryGetFaceAverages(this UTextureCube texture, Span faces) { if (texture.PlatformData.Mips is not { Length: > 0 } mips || mips[^1].BulkData?.Data is not { } data) return false; From 469ed4608d5c4d6fd525c774327998157e7a0400 Mon Sep 17 00:00:00 2001 From: Asval Date: Thu, 5 Nov 2026 19:39:26 +0100 Subject: [PATCH 3/4] bump --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index badffcd..f7303f5 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit badffcd0421a468667e7253fa3387b21f451af5d +Subproject commit f7303f56da41b9bc43b983e48ecc4644a684c310 From 9682225ab6da0033efe3b90941b4309a962f5c5b Mon Sep 17 00:00:00 2001 From: Marlon Date: Thu, 5 Nov 2026 20:02:35 +0100 Subject: [PATCH 4/4] fix: upload BC6H signed and EAC R11/RG11 to the GPU These formats had no GL mapping, so they went through the CPU decoder. For BC6H signed, that path also wrote absolute values into 8 bits, which dropped the sign and the HDR range. The GL formats for all of them are core and decode the same data. Co-Authored-By: Claude Opus 5.5 --- Snooper/Extensions/TextureExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Snooper/Extensions/TextureExtensions.cs b/Snooper/Extensions/TextureExtensions.cs index 9131e33..6eda8bb 100644 --- a/Snooper/Extensions/TextureExtensions.cs +++ b/Snooper/Extensions/TextureExtensions.cs @@ -138,6 +138,7 @@ bool Swizzle(ReadOnlySpan layout, out EMaskChannels absent) EPixelFormat.PF_BC4 => Blocks(SizedInternalFormat.CompressedRedRgtc1), EPixelFormat.PF_BC5 => Blocks(SizedInternalFormat.CompressedRgRgtc2), EPixelFormat.PF_BC6H => Blocks(SizedInternalFormat.CompressedRgbBptcUnsignedFloat), + EPixelFormat.PF_BC6H_Signed => Blocks(SizedInternalFormat.CompressedRgbBptcSignedFloat), EPixelFormat.PF_BC7 => Blocks(SizedInternalFormat.CompressedRgbaBptcUnorm, SizedInternalFormat.CompressedSrgbAlphaBptcUnorm), EPixelFormat.PF_ASTC_4x4 when DeviceInfo.HasAstc => Blocks(SizedInternalFormat.CompressedRgbaAstc4X4, SizedInternalFormat.CompressedSrgb8Alpha8Astc4X4), @@ -152,6 +153,8 @@ bool Swizzle(ReadOnlySpan layout, out EMaskChannels absent) EPixelFormat.PF_ETC1 => Blocks(SizedInternalFormat.CompressedRgb8Etc2, SizedInternalFormat.CompressedSrgb8Etc2), EPixelFormat.PF_ETC2_RGB => Blocks(SizedInternalFormat.CompressedRgb8Etc2, SizedInternalFormat.CompressedSrgb8Etc2), EPixelFormat.PF_ETC2_RGBA => Blocks(SizedInternalFormat.CompressedRgba8Etc2Eac, SizedInternalFormat.CompressedSrgb8Alpha8Etc2Eac), + EPixelFormat.PF_ETC2_R11 or EPixelFormat.PF_ETC2_R11_EAC => Blocks(SizedInternalFormat.CompressedR11Eac), + EPixelFormat.PF_ETC2_RG11_EAC => Blocks(SizedInternalFormat.CompressedRg11Eac), _ => null };