From 27de2235416868ee22a8d51fcfc7541258a31cb6 Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Sun, 6 Apr 2025 15:00:23 -0800 Subject: [PATCH 1/7] Fixes "Mipmap not found in texture" exception for textures with a height that isn't a multiple of 4 but is compressed. --- CodeWalker.Core/GameFiles/Resources/Texture.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 36c558ee9..2061ef4a4 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -707,6 +707,8 @@ public int CalcDataSize() len += slicePitch; div *= 2; } + + Console.WriteLine(len * Depth); return len * Depth; } public ushort CalculateStride() @@ -1099,6 +1101,13 @@ public override void Read(ResourceDataReader reader, params object[] parameters) int Levels = Convert.ToInt32(parameters[3]); int Stride = Convert.ToInt32(parameters[4]); + bool compressed = DDSIO.DXTex.IsCompressed(DDSIO.GetDXGIFormat((TextureFormat)format)); + + if (compressed && Height % 4 != 0) + { + Height = Math.Max(1, (Height + 3) & ~3); + } + int fullLength = 0; int length = Stride * Height; for (int i = 0; i < Levels; i++) From bbd0998e13a06b946b7cc168e11126069be8ec40 Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Sun, 6 Apr 2025 15:06:25 -0800 Subject: [PATCH 2/7] Remove extra logging that was left in from initially debugging the issue. --- CodeWalker.Core/GameFiles/Resources/Texture.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 2061ef4a4..d441bba0e 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -708,7 +708,6 @@ public int CalcDataSize() div *= 2; } - Console.WriteLine(len * Depth); return len * Depth; } public ushort CalculateStride() From c26824be968b6975185ef28ce8c96c58eeec5fc4 Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Sun, 6 Apr 2025 15:41:13 -0800 Subject: [PATCH 3/7] Change Math.Max(1 to Math.Max(4 as minimum dimension should be 4 not 1. --- CodeWalker.Core/GameFiles/Resources/Texture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index d441bba0e..088acf0b1 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -1104,7 +1104,7 @@ public override void Read(ResourceDataReader reader, params object[] parameters) if (compressed && Height % 4 != 0) { - Height = Math.Max(1, (Height + 3) & ~3); + Height = Math.Max(4, (Height + 3) & ~3); } int fullLength = 0; From 1dc7176190f2ab8213a2f2fbd46e70aa2450e62f Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Mon, 7 Apr 2025 23:25:54 -0800 Subject: [PATCH 4/7] Fixes: patches several additional DDS errors and oversights --- .../GameFiles/Resources/Texture.cs | 56 +++++++++++++++---- CodeWalker.Core/GameFiles/Utils/DDSIO.cs | 23 ++++---- CodeWalker/Forms/YtdForm.cs | 6 +- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 088acf0b1..8f69f6d4e 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -1095,24 +1095,58 @@ public override void Read(ResourceDataReader reader, params object[] parameters) else { uint format = Convert.ToUInt32(parameters[0]); - int Width = Convert.ToInt32(parameters[1]); - int Height = Convert.ToInt32(parameters[2]); - int Levels = Convert.ToInt32(parameters[3]); - int Stride = Convert.ToInt32(parameters[4]); + int width = Convert.ToInt32(parameters[1]); + int height = Convert.ToInt32(parameters[2]); + int levels = Convert.ToInt32(parameters[3]); + int stride = Convert.ToInt32(parameters[4]); bool compressed = DDSIO.DXTex.IsCompressed(DDSIO.GetDXGIFormat((TextureFormat)format)); - - if (compressed && Height % 4 != 0) + + if (compressed && height % 4 != 0) { - Height = Math.Max(4, (Height + 3) & ~3); + height = Math.Max(4, (height + 3) & ~3); } - + + // Manually compute pitch/stride so we don't rely on what's contained in the DDS Header/Texture + // parameters as I've encountered a number of files with incorrectly computed strides lately. In + // particular a handful of ATI2 textures. + DDSIO.DXTex.ComputePitch(DDSIO.GetDXGIFormat((TextureFormat)format), width, height, out int _, out int length, 0); + int fullLength = 0; - int length = Stride * Height; - for (int i = 0; i < Levels; i++) + for (int i = 0; i < levels; i++) { + // Length should only be divided by an amount relative to how much the mipmap dimensions actually + // decreased by. + int previousWidth = width; + int previousHeight = height; + width = Math.Max(1, width / 2); + height = Math.Max(1, height / 2); fullLength += length; - length /= 4; + int div = 0; + + if (previousWidth != width) + { + div += 2; + } + + if (previousHeight != height) + { + div += 2; + } + + if (div == 0) + { + div = 1; + } + + length /= div; + + // Compressed texture mipmaps should never contain less than 1 4x4 block (16 pixels) + // so length should be constrained to never be less than bits per pixel multiplied by 16 pixels and + // divided by 8 to get block length in bytes. Uncompressed texture mipmaps should never contain less + // than 1 pixel's worth of data. + length = Math.Max(DDSIO.DXTex.BitsPerPixel(DDSIO.GetDXGIFormat((TextureFormat)format)) * + (compressed ? 16 : 1)/8, length); } FullData = reader.ReadBytes(fullLength); diff --git a/CodeWalker.Core/GameFiles/Utils/DDSIO.cs b/CodeWalker.Core/GameFiles/Utils/DDSIO.cs index 285487e9d..1863ce711 100644 --- a/CodeWalker.Core/GameFiles/Utils/DDSIO.cs +++ b/CodeWalker.Core/GameFiles/Utils/DDSIO.cs @@ -544,8 +544,10 @@ private static Image[] GetMipmapImages(ImageStruct img, DXGI_FORMAT format) int add = 0; for (int i = 0; i < img.MipMapLevels; i++) { - images[i].width = img.Width / div; - images[i].height = img.Height / div; + // Prevent width or height from becoming zero. One divided by anything will be < 1 which when cast to an + // int will be truncated to 0. + images[i].width = Math.Max(1, img.Width / div); + images[i].height = Math.Max(1, img.Height / div); images[i].format = format; //(DXGI_FORMAT)img.Format; images[i].pixels = buf + add; @@ -2701,32 +2703,33 @@ private static void DecompressBC5Block(BinaryReader imageReader, int x, int y, i r = (byte)(((6 - rIndex) * r0 + (rIndex - 1) * r1) / 5); } - + // Fixed copy and paste error? Most of these 'g's below were 'r's, which caused BC5 (ATI2) textures + // to not have correct colors. byte g = 255; uint gIndex = (uint)((gMask >> 3 * (4 * blockY + blockX)) & 0x07); if (gIndex == 0) { - r = r0; + g = g0; } else if (gIndex == 1) { - r = r1; + g = g1; } - else if (r0 > r1) + else if (g0 > g1) { - r = (byte)(((8 - gIndex) * r0 + (gIndex - 1) * r1) / 7); + g = (byte)(((8 - gIndex) * g0 + (gIndex - 1) * g1) / 7); } else if (gIndex == 6) { - r = 0; + g = 0; } else if (gIndex == 7) { - r = 0xff; + g = 0xff; } else { - r = (byte)(((6 - gIndex) * r0 + (gIndex - 1) * r1) / 5); + g = (byte)(((6 - gIndex) * g0 + (gIndex - 1) * g1) / 5); } diff --git a/CodeWalker/Forms/YtdForm.cs b/CodeWalker/Forms/YtdForm.cs index c966d7ec6..b8775d335 100644 --- a/CodeWalker/Forms/YtdForm.cs +++ b/CodeWalker/Forms/YtdForm.cs @@ -153,8 +153,10 @@ private void ShowTextureMip(Texture tex, int mip, bool mipchange) { int cmip = Math.Min(Math.Max(mip, 0), tex.Levels - 1); byte[] pixels = DDSIO.GetPixels(tex, cmip); - int w = tex.Width >> cmip; - int h = tex.Height >> cmip; + // Certain mipmap dimension reduction chains may lead this to become zero, so it should be clamped to be + // one at minimum. + int w = Math.Max(1, tex.Width >> cmip); + int h = Math.Max(1, tex.Height >> cmip); Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb); if (pixels != null) From 796f02e5acfcfb56cde96e260136cb1cc5af7f0e Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Tue, 8 Apr 2025 10:33:00 -0800 Subject: [PATCH 5/7] Actually modify the stride of the texture so that it uses the correct value. --- .../GameFiles/Resources/Texture.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 8f69f6d4e..2ac54f1e6 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -938,7 +938,12 @@ public override void Read(ResourceDataReader reader, params object[] parameters) this.Unknown_84h = reader.ReadUInt32(); this.Unknown_88h = reader.ReadUInt32(); this.Unknown_8Ch = reader.ReadUInt32(); - + + // Ignore stride loaded from file as it may be incorrect, especially if texture is ATI2 and the file was + // previously saved in OpenIV + DDSIO.DXTex.ComputePitch(DDSIO.GetDXGIFormat(this.Format), this.Width, this.Height, out int stride, out int _, 0); + this.Stride = (ushort)stride; + // read reference data this.Data = reader.ReadBlockAt(this.DataPointer, this.Format, this.Width, this.Height, this.Levels, this.Stride); @@ -1102,17 +1107,15 @@ public override void Read(ResourceDataReader reader, params object[] parameters) bool compressed = DDSIO.DXTex.IsCompressed(DDSIO.GetDXGIFormat((TextureFormat)format)); - if (compressed && height % 4 != 0) + // For compressed textures stride should be multiplied by the number of vertical blocks, not the height + // of the texture. + if (compressed) { - height = Math.Max(4, (height + 3) & ~3); + height = Math.Max(1, (height + 3) / 4); } - // Manually compute pitch/stride so we don't rely on what's contained in the DDS Header/Texture - // parameters as I've encountered a number of files with incorrectly computed strides lately. In - // particular a handful of ATI2 textures. - DDSIO.DXTex.ComputePitch(DDSIO.GetDXGIFormat((TextureFormat)format), width, height, out int _, out int length, 0); - int fullLength = 0; + int length = stride * height; for (int i = 0; i < levels; i++) { // Length should only be divided by an amount relative to how much the mipmap dimensions actually From 935c3632d2d0a09f269074fbe4396b2bad5ef1b4 Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Tue, 8 Apr 2025 10:54:33 -0800 Subject: [PATCH 6/7] Minor optimization: don't recalculate minimum mipmap length for every mipmap. --- CodeWalker.Core/GameFiles/Resources/Texture.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 2ac54f1e6..5ba5022c0 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -1113,6 +1113,9 @@ public override void Read(ResourceDataReader reader, params object[] parameters) { height = Math.Max(1, (height + 3) / 4); } + + int minimumLengthPerMip = DDSIO.DXTex.BitsPerPixel(DDSIO.GetDXGIFormat((TextureFormat)format)) * + (compressed ? 16 : 1) / 8; int fullLength = 0; int length = stride * height; @@ -1147,9 +1150,8 @@ public override void Read(ResourceDataReader reader, params object[] parameters) // Compressed texture mipmaps should never contain less than 1 4x4 block (16 pixels) // so length should be constrained to never be less than bits per pixel multiplied by 16 pixels and // divided by 8 to get block length in bytes. Uncompressed texture mipmaps should never contain less - // than 1 pixel's worth of data. - length = Math.Max(DDSIO.DXTex.BitsPerPixel(DDSIO.GetDXGIFormat((TextureFormat)format)) * - (compressed ? 16 : 1)/8, length); + // than 1 pixel's worth of data. + length = Math.Max(minimumLengthPerMip, length); } FullData = reader.ReadBytes(fullLength); From c99a758bf632fdaad4d5f10bc84fb289588157bc Mon Sep 17 00:00:00 2001 From: JacksonAbney Date: Sat, 10 May 2025 18:46:56 -0800 Subject: [PATCH 7/7] Refactor patch to use CalcDataSize and unify G9 and legacy data loading. --- .../GameFiles/Resources/Texture.cs | 91 ++++--------------- 1 file changed, 19 insertions(+), 72 deletions(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Texture.cs b/CodeWalker.Core/GameFiles/Resources/Texture.cs index 5ba5022c0..c92c00857 100644 --- a/CodeWalker.Core/GameFiles/Resources/Texture.cs +++ b/CodeWalker.Core/GameFiles/Resources/Texture.cs @@ -699,17 +699,27 @@ public int CalcDataSize() var dxgifmt = DDSIO.GetDXGIFormat(Format); int div = 1; int len = 0; + bool compressed = DDSIO.DXTex.IsCompressed(dxgifmt); + int minimumLengthPerMip = compressed ? (IsBC1Based(dxgifmt) ? 8 : 16) : DDSIO.DXTex.BitsPerPixel(dxgifmt) / 8; for (int i = 0; i < Levels; i++) { - var width = Width / div; - var height = Height / div; + // Width or Height may reach 1 before the last mip level, half of 1 would be 0.5 truncated to 0. + // A texture can't have a dimension of 0, so we need to floor at 1. + var width = Math.Max(1, Width / div); + var height = Math.Max(1, Height / div); DDSIO.DXTex.ComputePitch(dxgifmt, width, height, out var rowPitch, out var slicePitch, 0); - len += slicePitch; + len += Math.Max(minimumLengthPerMip, slicePitch); div *= 2; } return len * Depth; } + + private bool IsBC1Based(DDSIO.DXGI_FORMAT format) + { + return format == DDSIO.DXGI_FORMAT.DXGI_FORMAT_BC1_TYPELESS || format == DDSIO.DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM || format == DDSIO.DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM_SRGB; + } + public ushort CalculateStride() { if (Format == 0) return 0; @@ -940,12 +950,13 @@ public override void Read(ResourceDataReader reader, params object[] parameters) this.Unknown_8Ch = reader.ReadUInt32(); // Ignore stride loaded from file as it may be incorrect, especially if texture is ATI2 and the file was - // previously saved in OpenIV - DDSIO.DXTex.ComputePitch(DDSIO.GetDXGIFormat(this.Format), this.Width, this.Height, out int stride, out int _, 0); - this.Stride = (ushort)stride; + // previously saved in OpenIV, DDS documentation recommends recalculating this anyway + DDSIO.DXTex.ComputePitch(DDSIO.GetDXGIFormat(Format), this.Width, this.Height, out int rowPitch, out int slicePitch, 0); + this.Stride = (ushort)rowPitch; // read reference data - this.Data = reader.ReadBlockAt(this.DataPointer, this.Format, this.Width, this.Height, this.Levels, this.Stride); + this.Data = reader.ReadBlockAt(this.DataPointer, CalcDataSize()); + //this.Format, this.Width, this.Height, this.Levels, this.Stride); } } @@ -1092,71 +1103,7 @@ public override long BlockLength /// public override void Read(ResourceDataReader reader, params object[] parameters) { - if (reader.IsGen9) - { - int fullLength = Convert.ToInt32(parameters[0]); - FullData = reader.ReadBytes(fullLength); - } - else - { - uint format = Convert.ToUInt32(parameters[0]); - int width = Convert.ToInt32(parameters[1]); - int height = Convert.ToInt32(parameters[2]); - int levels = Convert.ToInt32(parameters[3]); - int stride = Convert.ToInt32(parameters[4]); - - bool compressed = DDSIO.DXTex.IsCompressed(DDSIO.GetDXGIFormat((TextureFormat)format)); - - // For compressed textures stride should be multiplied by the number of vertical blocks, not the height - // of the texture. - if (compressed) - { - height = Math.Max(1, (height + 3) / 4); - } - - int minimumLengthPerMip = DDSIO.DXTex.BitsPerPixel(DDSIO.GetDXGIFormat((TextureFormat)format)) * - (compressed ? 16 : 1) / 8; - - int fullLength = 0; - int length = stride * height; - for (int i = 0; i < levels; i++) - { - // Length should only be divided by an amount relative to how much the mipmap dimensions actually - // decreased by. - int previousWidth = width; - int previousHeight = height; - width = Math.Max(1, width / 2); - height = Math.Max(1, height / 2); - fullLength += length; - int div = 0; - - if (previousWidth != width) - { - div += 2; - } - - if (previousHeight != height) - { - div += 2; - } - - if (div == 0) - { - div = 1; - } - - length /= div; - - // Compressed texture mipmaps should never contain less than 1 4x4 block (16 pixels) - // so length should be constrained to never be less than bits per pixel multiplied by 16 pixels and - // divided by 8 to get block length in bytes. Uncompressed texture mipmaps should never contain less - // than 1 pixel's worth of data. - length = Math.Max(minimumLengthPerMip, length); - } - - FullData = reader.ReadBytes(fullLength); - } - + FullData = reader.ReadBytes((int)parameters[0]); } ///