From 2cd27875330cb7ed5a9bf019722f39d90f7b6f25 Mon Sep 17 00:00:00 2001 From: mspielberg <9729801+mspielberg@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:45:29 -0800 Subject: [PATCH 1/4] Use BC5 compression for normal maps BC5 is a DX10 feature, requiring additional different headers in the DDS file used for caching. BC5 offers much better normal map quality than DXT1, which is currently used, and still achieves 75% compression compared to raw RGBA. --- SkinManagerMod/StbImage.cs | 15 ++- SkinManagerMod/StbImage.dll | Bin 103936 -> 104448 bytes SkinManagerMod/TextureLoader.cs | 195 ++++++++++++++++++++++++-------- 3 files changed, 162 insertions(+), 48 deletions(-) diff --git a/SkinManagerMod/StbImage.cs b/SkinManagerMod/StbImage.cs index cfbe668..63a0900 100644 --- a/SkinManagerMod/StbImage.cs +++ b/SkinManagerMod/StbImage.cs @@ -10,10 +10,17 @@ public static class StbImage [DllImport(Lib, EntryPoint = nameof(GetImageInfo))] private static extern int GetImageInfo(string filename, out uint x, out uint y, out uint comp); [DllImport(Lib, EntryPoint = nameof(ReadImageAsBCx))] - private static extern int ReadImageAsBCx(string filename, int flipVertically, int useAlpha, IntPtr dest, int destSize); + private static extern int ReadImageAsBCx(string filename, int flipVertically, int format, IntPtr dest, int destSize); [DllImport(Lib, EntryPoint = nameof(ReadImageAsRGBA))] private static extern int ReadImageAsRGBA(string filename, int flipVertically, IntPtr dest, int destSize); + public enum TextureFormat + { + BC1 = 1, + BC3 = 3, + BC5 = 5, + } + public struct ImageInfo { public int height; @@ -34,9 +41,11 @@ public static ImageInfo GetImageInfo(string filename) }; } - public static void ReadAndCompressImageWithMipmaps(string filename, bool flipVertically, bool useAlpha, IntPtr dest, int destSize) + public static void ReadAndCompressImageWithMipmaps(string filename, bool flipVertically, TextureFormat format, IntPtr dest, int destSize) { - bool success = ReadImageAsBCx(filename, flipVertically ? 1 : 0, useAlpha ? 1 : 0, dest, destSize) != 0; + if (Array.IndexOf(Enum.GetValues(typeof(TextureFormat)), format) < 0) + throw new ArgumentOutOfRangeException("format", $"Unknown texture format {format}"); + bool success = ReadImageAsBCx(filename, flipVertically ? 1 : 0, (int)format, dest, destSize) != 0; if (!success) throw new Exception($"Unable to read {filename}"); } diff --git a/SkinManagerMod/StbImage.dll b/SkinManagerMod/StbImage.dll index 9ff8276b003cd2d4c87426924e0ec87cd2c9e945..edf220079e98559685c37b81031b94a74344a4eb 100644 GIT binary patch delta 19152 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 delta 18342 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 MzZTS9#jpAOFOOJh8UO$Q diff --git a/SkinManagerMod/TextureLoader.cs b/SkinManagerMod/TextureLoader.cs index a2e4346..28ecac0 100644 --- a/SkinManagerMod/TextureLoader.cs +++ b/SkinManagerMod/TextureLoader.cs @@ -53,22 +53,25 @@ private static Task TryLoadFromCache(ResourceConfigJson skin, string } } - public static Task LoadAsync(ResourceConfigJson skin, string texturePath, bool linear) + public static Task LoadAsync(ResourceConfigJson skin, string texturePath, bool isNormalMap) { - var cached = TryLoadFromCache(skin, texturePath, linear); + var cached = TryLoadFromCache(skin, texturePath, isNormalMap); if (!cached.IsCompleted || cached.Result != null) { return cached; } var info = StbImage.GetImageInfo(texturePath); - var texture = new Texture2D(info.width, info.height, - info.componentCount > 3 ? TextureFormat.DXT5 : TextureFormat.DXT1, - mipChain: true, linear); + var format = isNormalMap ? TextureFormat.BC5 : + info.componentCount > 3 ? TextureFormat.DXT5 : + TextureFormat.DXT1; + + var texture = new Texture2D(info.width, info.height, format, + mipChain: true, linear: isNormalMap); var nativeArray = texture.GetRawTextureData(); return Task.Run(() => { - PopulateTexture(texturePath, info.componentCount > 3, nativeArray); + PopulateTexture(texturePath, format, nativeArray); string cachePath = GetCachePath(skin, texturePath); Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); DDSUtils.WriteDDSGz(new FileInfo(cachePath), texture); @@ -76,9 +79,9 @@ public static Task LoadAsync(ResourceConfigJson skin, string textureP }); } - public static Texture2D LoadSync(ResourceConfigJson skin, string texturePath, bool linear) + public static Texture2D LoadSync(ResourceConfigJson skin, string texturePath, bool isNormalMap) { - var texture = new Texture2D(0, 0, textureFormat: TextureFormat.RGBA32, mipChain: true, linear: linear); + var texture = new Texture2D(0, 0, textureFormat: TextureFormat.RGBA32, mipChain: true, linear: isNormalMap); texture.LoadImage(File.ReadAllBytes(texturePath)); return texture; @@ -91,14 +94,30 @@ private static string GetCachePath(ResourceConfigJson skin, string texturePath) return Path.Combine(Main.CacheFolderPath, skin.CarId, skin.Name, cacheFileName); } - private static void PopulateTexture(string path, bool hasAlpha, NativeArray dest) + private static void PopulateTexture(string path, TextureFormat textureFormat, NativeArray dest) { + StbImage.TextureFormat format; + switch (textureFormat) + { + case TextureFormat.DXT1: + format = StbImage.TextureFormat.BC1; + break; + case TextureFormat.DXT5: + format = StbImage.TextureFormat.BC3; + break; + case TextureFormat.BC5: + format = StbImage.TextureFormat.BC5; + break; + default: + throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); + } + unsafe { StbImage.ReadAndCompressImageWithMipmaps( path, flipVertically: true, - useAlpha: hasAlpha, + format, (IntPtr)dest.GetUnsafePtr(), dest.Length); } @@ -112,14 +131,28 @@ public DDSReadException(string message) : base(message) { } internal static class DDSUtils { - private static int Mipmap0SizeInBytes(int width, int height, bool hasAlpha) + private static int Mipmap0SizeInBytes(int width, int height, TextureFormat textureFormat) { var blockWidth = (width + 3) / 4; var blockHeight = (height + 3) / 4; - return blockWidth * blockHeight * (hasAlpha ? 16 : 8); + int bytesPerBlock; + switch (textureFormat) + { + case TextureFormat.DXT1: + bytesPerBlock = 8; + break; + case TextureFormat.DXT5: + case TextureFormat.BC5: + bytesPerBlock = 16; + break; + default: + throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); + } + + return blockWidth * blockHeight * bytesPerBlock; } - private static byte[] DDSHeader(int width, int height, bool hasAlpha, int numMipmaps) + private static byte[] DDSHeader(int width, int height, TextureFormat textureFormat, int numMipmaps) { var header = new byte[128]; using (var stream = new MemoryStream(header)) @@ -130,12 +163,12 @@ private static byte[] DDSHeader(int width, int height, bool hasAlpha, int numMip stream.Write(BitConverter.GetBytes(0x1 | 0x2 | 0x4 | 0x1000 | 0x20000 | 0x80000), 0, 4); stream.Write(BitConverter.GetBytes(height), 0, 4); stream.Write(BitConverter.GetBytes(width), 0, 4); - stream.Write(BitConverter.GetBytes(Mipmap0SizeInBytes(width, height, hasAlpha)), 0, 4); // dwPitchOrLinearSize + stream.Write(BitConverter.GetBytes(Mipmap0SizeInBytes(width, height, textureFormat)), 0, 4); // dwPitchOrLinearSize stream.Write(BitConverter.GetBytes(0), 0, 4); // dwDepth stream.Write(BitConverter.GetBytes(numMipmaps), 0, 4); // dwMipMapCount for (int i = 0; i < 11; i++) stream.Write(BitConverter.GetBytes(0), 0, 4); // dwReserved1 - var pixelFormat = PixelFormat(hasAlpha); + var pixelFormat = PixelFormat(textureFormat); stream.Write(pixelFormat, 0, pixelFormat.Length); // dwCaps = COMPLEX | MIPMAP | TEXTURE stream.Write(BitConverter.GetBytes(0x401008), 0, 4); @@ -143,29 +176,121 @@ private static byte[] DDSHeader(int width, int height, bool hasAlpha, int numMip return header; } - private static byte[] PixelFormat(bool hasAlpha) + private static byte[] PixelFormat(TextureFormat textureFormat) { + string fourCC; + switch (textureFormat) + { + case TextureFormat.DXT1: + fourCC = "DXT1"; + break; + case TextureFormat.DXT5: + fourCC = "DXT5"; + break; + default: + fourCC = "DX10"; + break; + } + var pixelFormat = new byte[32]; - var stream = new MemoryStream(pixelFormat); - stream.Write(BitConverter.GetBytes(32), 0, 4); // dwSize - stream.Write(BitConverter.GetBytes(0x4), 0, 4); // dwFlags = FOURCC - stream.Write(Encoding.ASCII.GetBytes(hasAlpha ? "DXT5" : "DXT1"), 0, 4); // dwFourCC - stream.Close(); + using (var stream = new MemoryStream(pixelFormat)) + { + stream.Write(BitConverter.GetBytes(32), 0, 4); // dwSize + stream.Write(BitConverter.GetBytes(0x4), 0, 4); // dwFlags = FOURCC + stream.Write(Encoding.ASCII.GetBytes(fourCC), 0, 4); // dwFourCC + } return pixelFormat; } + private static int DXGIFormat(TextureFormat textureFormat) + { + switch (textureFormat) + { + case TextureFormat.BC5: return 83; + default: + throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); + } + } + + private static byte[] DDSHeaderDXT10(TextureFormat textureFormat) + { + var headerDXT10 = new byte[20]; + using (var stream = new MemoryStream(headerDXT10)) + { + stream.Write(BitConverter.GetBytes(DXGIFormat(textureFormat)), 0, 4); // dxgiFormat + stream.Write(BitConverter.GetBytes(3), 0, 4); // resourceDimension = 3 = DDS_DIMENSION_TEXTURE2D + stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlag + stream.Write(BitConverter.GetBytes(1), 0, 4); // arraySize = 1 + stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlags2 = 0 = DDS_ALPHA_MODE_UNKNOWN + } + return headerDXT10; + } + public static void WriteDDSGz(FileInfo fileInfo, Texture2D texture) { Main.Log($"Writing to {fileInfo.FullName}"); using (var fileStream = fileInfo.OpenWrite()) using (var outfile = new GZipStream(fileStream, CompressionLevel.Optimal)) { - outfile.Write(DDSHeader(texture.width, texture.height, texture.format == TextureFormat.DXT5, texture.mipmapCount), 0, 128); + var header = DDSHeader(texture.width, texture.height, texture.format, texture.mipmapCount); + outfile.Write(header, 0, header.Length); + if (texture.format != TextureFormat.DXT1 && texture.format != TextureFormat.DXT5) + { + // compressed formats other than DXT1-5 require DX10 + var headerDXT10 = DDSHeaderDXT10(texture.format); + outfile.Write(headerDXT10, 0, headerDXT10.Length); + } var data = texture.GetRawTextureData().ToArray(); outfile.Write(data, 0, data.Length); } } + private static Texture2D ReadDDSHeader(Stream infile, bool linear) + { + var buf = new byte[4096]; + var bytesRead = infile.Read(buf, 0, 128); + if (bytesRead != 128 || Encoding.ASCII.GetString(buf, 0, 4) != "DDS ") + throw new DDSReadException("File is not a DDS file"); + + int height = BitConverter.ToInt32(buf, 12); + int width = BitConverter.ToInt32(buf, 16); + + int pixelFormatFlags = BitConverter.ToInt32(buf, 80); + if ((pixelFormatFlags & 0x4) == 0) + throw new DDSReadException("DDS header does not have a FourCC"); + string fourCC = Encoding.ASCII.GetString(buf, 84, 4); + TextureFormat textureFormat; + switch (fourCC) + { + case "DXT1": + textureFormat = TextureFormat.DXT1; + break; + case "DXT5": + textureFormat = TextureFormat.DXT5; + break; + case "DX10": + // read DDS_HEADER_DXT10 header extension + bytesRead = infile.Read(buf, 0, 20); + if (bytesRead != 20) + throw new DDSReadException("Could not read DXT10 header from DDS file"); + int dxgiFormat = BitConverter.ToInt32(buf, 0); + switch (dxgiFormat) + { + case 83: + textureFormat = TextureFormat.BC5; + break; + default: + throw new DDSReadException($"Unsupported DXGI_FORMAT {dxgiFormat}"); + } + break; + default: + throw new DDSReadException($"Unknown FourCC: {fourCC}"); + } + + var texture = new Texture2D(width, height, textureFormat, true, linear); + return texture; + } + public static Task ReadDDSGz(FileInfo fileInfo, bool linear) { FileStream fileStream = null; @@ -176,34 +301,14 @@ public static Task ReadDDSGz(FileInfo fileInfo, bool linear) fileStream = fileInfo.OpenRead(); infile = new GZipStream(fileStream, CompressionMode.Decompress); - var buf = new byte[4096]; - var bytesRead = infile.Read(buf, 0, 128); - if (bytesRead != 128 || Encoding.ASCII.GetString(buf, 0, 4) != "DDS ") - throw new DDSReadException("File is not a DDS file"); - - int height = BitConverter.ToInt32(buf, 12); - int width = BitConverter.ToInt32(buf, 16); - - int pixelFormatFlags = BitConverter.ToInt32(buf, 80); - if ((pixelFormatFlags & 0x4) == 0) - throw new DDSReadException("DDS header does not have a FourCC"); - string fourCC = Encoding.ASCII.GetString(buf, 84, 4); - TextureFormat pixelFormat; - switch (fourCC) - { - case "DXT1": pixelFormat = TextureFormat.DXT1; break; - case "DXT5": pixelFormat = TextureFormat.DXT5; break; - default: throw new DDSReadException($"Unknown FourCC: {fourCC}"); - } - - var texture = new Texture2D(width, height, pixelFormat, true, linear); + var texture = ReadDDSHeader(infile, linear); var nativeArray = texture.GetRawTextureData(); return Task.Run(() => { try { - buf = new byte[nativeArray.Length]; - bytesRead = infile.Read(buf, 0, nativeArray.Length); + var buf = new byte[nativeArray.Length]; + var bytesRead = infile.Read(buf, 0, nativeArray.Length); if (bytesRead < nativeArray.Length) throw new DDSReadException($"{fileInfo.FullName}: Expected {nativeArray.Length} bytes, but file contained {bytesRead}"); nativeArray.CopyFrom(buf); From 2836250284be74b0e65d874eb08c9d204772a7f3 Mon Sep 17 00:00:00 2001 From: mspielberg <9729801+mspielberg@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:14:10 -0800 Subject: [PATCH 2/4] Reload resource packs with forceSync Prior to this change the reload operation would use LoadSync for textures in skin directories, which uses Unity's LoadImage and RGBA32 format, but LoadAsync for resource packs, which uses StbImage. This changes to use LoadSync for both. --- SkinManagerMod/SkinProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SkinManagerMod/SkinProvider.cs b/SkinManagerMod/SkinProvider.cs index 9dfffb1..30f20b8 100644 --- a/SkinManagerMod/SkinProvider.cs +++ b/SkinManagerMod/SkinProvider.cs @@ -303,7 +303,7 @@ private static int ReloadSkinMod(UnityModManager.ModEntry mod, bool forceSync = { if (ResourcePack.LoadFromFile(file) is ResourcePack resourceConfig) { - BeginLoadResources(resourceConfig); + BeginLoadResources(resourceConfig, forceSync); newConfig.ResourcePacks.Add(resourceConfig); } } From f08c728d3345db778ed80ccca489b99350966c1c Mon Sep 17 00:00:00 2001 From: mspielberg <9729801+mspielberg@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:27:45 -0800 Subject: [PATCH 3/4] Fold DXT10 header writing into DDSHeader --- SkinManagerMod/TextureLoader.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/SkinManagerMod/TextureLoader.cs b/SkinManagerMod/TextureLoader.cs index 28ecac0..66e86d8 100644 --- a/SkinManagerMod/TextureLoader.cs +++ b/SkinManagerMod/TextureLoader.cs @@ -152,9 +152,13 @@ private static int Mipmap0SizeInBytes(int width, int height, TextureFormat textu return blockWidth * blockHeight * bytesPerBlock; } + private const int DDS_HEADER_SIZE = 128; + private const int DDS_HEADER_DXT10_SIZE = 20; private static byte[] DDSHeader(int width, int height, TextureFormat textureFormat, int numMipmaps) { - var header = new byte[128]; + var needsDXGIHeader = textureFormat != TextureFormat.DXT1 && textureFormat != TextureFormat.DXT5; + var headerSize = needsDXGIHeader ? DDS_HEADER_SIZE + DDS_HEADER_DXT10_SIZE : DDS_HEADER_SIZE; + var header = new byte[headerSize]; using (var stream = new MemoryStream(header)) { stream.Write(Encoding.ASCII.GetBytes("DDS "), 0, 4); @@ -172,6 +176,9 @@ private static byte[] DDSHeader(int width, int height, TextureFormat textureForm stream.Write(pixelFormat, 0, pixelFormat.Length); // dwCaps = COMPLEX | MIPMAP | TEXTURE stream.Write(BitConverter.GetBytes(0x401008), 0, 4); + + if (needsDXGIHeader) + stream.Write(DDSHeaderDXT10(textureFormat), 0, DDS_HEADER_DXT10_SIZE); } return header; } @@ -214,7 +221,7 @@ private static int DXGIFormat(TextureFormat textureFormat) private static byte[] DDSHeaderDXT10(TextureFormat textureFormat) { - var headerDXT10 = new byte[20]; + var headerDXT10 = new byte[DDS_HEADER_DXT10_SIZE]; using (var stream = new MemoryStream(headerDXT10)) { stream.Write(BitConverter.GetBytes(DXGIFormat(textureFormat)), 0, 4); // dxgiFormat @@ -234,12 +241,7 @@ public static void WriteDDSGz(FileInfo fileInfo, Texture2D texture) { var header = DDSHeader(texture.width, texture.height, texture.format, texture.mipmapCount); outfile.Write(header, 0, header.Length); - if (texture.format != TextureFormat.DXT1 && texture.format != TextureFormat.DXT5) - { - // compressed formats other than DXT1-5 require DX10 - var headerDXT10 = DDSHeaderDXT10(texture.format); - outfile.Write(headerDXT10, 0, headerDXT10.Length); - } + var data = texture.GetRawTextureData().ToArray(); outfile.Write(data, 0, data.Length); } @@ -248,7 +250,7 @@ public static void WriteDDSGz(FileInfo fileInfo, Texture2D texture) private static Texture2D ReadDDSHeader(Stream infile, bool linear) { var buf = new byte[4096]; - var bytesRead = infile.Read(buf, 0, 128); + var bytesRead = infile.Read(buf, 0, DDS_HEADER_SIZE); if (bytesRead != 128 || Encoding.ASCII.GetString(buf, 0, 4) != "DDS ") throw new DDSReadException("File is not a DDS file"); @@ -270,8 +272,8 @@ private static Texture2D ReadDDSHeader(Stream infile, bool linear) break; case "DX10": // read DDS_HEADER_DXT10 header extension - bytesRead = infile.Read(buf, 0, 20); - if (bytesRead != 20) + bytesRead = infile.Read(buf, 0, DDS_HEADER_DXT10_SIZE); + if (bytesRead != DDS_HEADER_DXT10_SIZE) throw new DDSReadException("Could not read DXT10 header from DDS file"); int dxgiFormat = BitConverter.ToInt32(buf, 0); switch (dxgiFormat) From ec9c44c338a548b73cb5be2d21c3a021caefcfc4 Mon Sep 17 00:00:00 2001 From: mspielberg <9729801+mspielberg@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:33:01 -0800 Subject: [PATCH 4/4] Ignore old cached normal maps If there is a cached DDS of a normal map from a prior version still using DXT1 or DXT5 compression, ignore it and regenerate with BC5 compression. --- SkinManagerMod/TextureLoader.cs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/SkinManagerMod/TextureLoader.cs b/SkinManagerMod/TextureLoader.cs index 66e86d8..5a7b91b 100644 --- a/SkinManagerMod/TextureLoader.cs +++ b/SkinManagerMod/TextureLoader.cs @@ -25,7 +25,7 @@ public static void BustCache(ResourceConfigJson skin, string texturePath) } } - private static Task TryLoadFromCache(ResourceConfigJson skin, string texturePath, bool linear) + private static Task TryLoadFromCache(ResourceConfigJson skin, string texturePath, bool isNormalMap) { var texFile = new FileInfo(texturePath); var cached = new FileInfo(GetCachePath(skin, texturePath)); @@ -37,17 +37,18 @@ private static Task TryLoadFromCache(ResourceConfigJson skin, string if (cached.LastWriteTimeUtc < texFile.LastWriteTimeUtc) { - cached.Delete(); + Main.LogVerbose($"Cached texture {cached.FullName} is out of date"); + BustCache(skin, texturePath); return Task.FromResult(null); } try { - return DDSUtils.ReadDDSGz(cached, linear); + return DDSUtils.ReadDDSGz(cached, isNormalMap); } catch (DDSReadException e) { - Main.Warning($"Error loading cached skin {skin.Name}: {e.Message}"); + Main.Warning($"Error loading cached texture {cached.FullName}: {e.Message}"); BustCache(skin, texturePath); return Task.FromResult(null); } @@ -69,6 +70,7 @@ public static Task LoadAsync(ResourceConfigJson skin, string textureP var texture = new Texture2D(info.width, info.height, format, mipChain: true, linear: isNormalMap); var nativeArray = texture.GetRawTextureData(); + Main.LogVerbose($"Loading texture {texturePath} as {format} with StbImage"); return Task.Run(() => { PopulateTexture(texturePath, format, nativeArray); @@ -81,7 +83,9 @@ public static Task LoadAsync(ResourceConfigJson skin, string textureP public static Texture2D LoadSync(ResourceConfigJson skin, string texturePath, bool isNormalMap) { - var texture = new Texture2D(0, 0, textureFormat: TextureFormat.RGBA32, mipChain: true, linear: isNormalMap); + var textureFormat = TextureFormat.RGBA32; + var texture = new Texture2D(0, 0, textureFormat, mipChain: true, linear: isNormalMap); + Main.LogVerbose($"Loading texture {texturePath} as {textureFormat} with LoadImage"); texture.LoadImage(File.ReadAllBytes(texturePath)); return texture; @@ -293,17 +297,26 @@ private static Texture2D ReadDDSHeader(Stream infile, bool linear) return texture; } - public static Task ReadDDSGz(FileInfo fileInfo, bool linear) + public static Task ReadDDSGz(FileInfo fileInfo, bool isNormalMap) { FileStream fileStream = null; GZipStream infile = null; try { - Main.LogVerbose($"Reading from {fileInfo.FullName}"); fileStream = fileInfo.OpenRead(); infile = new GZipStream(fileStream, CompressionMode.Decompress); - var texture = ReadDDSHeader(infile, linear); + var texture = ReadDDSHeader(infile, isNormalMap); + if (isNormalMap && texture.format != TextureFormat.BC5) + { + Main.LogVerbose($"Cached normal map texture {fileInfo.FullName} has old format {texture.format}"); + infile.Close(); + fileStream.Close(); + File.Delete(fileInfo.FullName); + return Task.FromResult(null); + } + + Main.LogVerbose($"Reading cached {texture.format} texture from {fileInfo.FullName}"); var nativeArray = texture.GetRawTextureData(); return Task.Run(() => {