From e5620e17e0f468ab6331e9dbf347e881c592cb5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 01:20:36 +0100 Subject: [PATCH] Improve texture saving --- Penumbra/Import/Textures/TextureManager.cs | 43 ++++++++++++------ Penumbra/Penumbra.csproj | 4 ++ .../Materials/MtrlTab.LivePreview.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 21 +++++++-- Penumbra/lib/OtterTex.dll | Bin 41984 -> 42496 bytes 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 7118f8af..6adf5861 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -6,15 +7,17 @@ using OtterGui.Log; using OtterGui.Services; using OtterGui.Tasks; using OtterTex; +using SharpDX.Direct3D11; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; +using DxgiDevice = SharpDX.DXGI.Device; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider) +public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder) : SingleTaskQueue, IDisposable, IService { private readonly Logger _logger = logger; @@ -47,11 +50,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, input, output)); public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, image, path, rgba, width, height)); private Task Enqueue(IAction action) { @@ -156,27 +159,30 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur private readonly string _outputPath; private readonly ImageInputData _input; private readonly CombinedTexture.TextureSaveType _type; + private readonly Device? _device; private readonly bool _mipMaps; private readonly bool _asTex; - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, - string output) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + string input, string output) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, - string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } @@ -201,8 +207,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, _device, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, _device, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -320,8 +326,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, - int width = 0, int height = 0) + public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel, + byte[]? rgba = null, int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) { @@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, cancel); + return CreateCompressed(dds, mipMaps, bc7, device, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, cancel); + return CreateCompressed(scratch, mipMaps, bc7, device, cancel); } default: return new BaseImage(); } @@ -384,7 +390,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel) { var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) @@ -398,6 +404,15 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur input = AddMipMaps(input, mipMaps); cancel.ThrowIfCancellationRequested(); + // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. + if (device is not null && format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + { + var dxgiDevice = device.QueryInterface(); + + using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); + return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); + } + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b4266aeb..870865da 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -80,6 +80,10 @@ $(DalamudLibPath)SharpDX.Direct3D11.dll False + + $(DalamudLibPath)SharpDX.DXGI.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 01a40980..5025bafd 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -138,7 +138,7 @@ public partial class MtrlTab foreach (var constant in Mtrl.ShaderPackage.Constants) { var values = Mtrl.GetConstantValue(constant); - if (values != null) + if (values != []) SetMaterialParameter(constant.Id, 0, values); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index c08e8a8e..d0764808 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -134,7 +134,7 @@ public partial class ModEditWindow tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -159,7 +159,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -169,7 +169,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -180,7 +180,7 @@ public partial class ModEditWindow || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } } @@ -235,7 +235,7 @@ public partial class ModEditWindow if (a) { _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, b); + AddChangeTask(b); if (b == _left.Path) AddReloadTask(_left.Path, false); else if (b == _right.Path) @@ -245,6 +245,17 @@ public partial class ModEditWindow _forceTextureStartPath = false; } + private void AddChangeTask(string path) + { + _center.SaveTask.ContinueWith(t => + { + if (!t.IsCompletedSuccessfully) + return; + + _framework.RunOnFrameworkThread(() => InvokeChange(Mod, path)); + }, TaskScheduler.Default); + } + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 29912e6215ff28a7959b73aa1e00cc9ebb919d76..c137aee184d789102648a05ecc24c0437df54ba1 100644 GIT binary patch delta 16738 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 delta 16281 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