diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs index e63f86376..18eabbbc8 100644 --- a/Penumbra/Collections/Cache/CustomResourceCache.cs +++ b/Penumbra/Collections/Cache/CustomResourceCache.cs @@ -1,7 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Luna; using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; -using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Config/Configuration.cs b/Penumbra/Config/Configuration.cs index ddde2f2a5..7e117ac38 100644 --- a/Penumbra/Config/Configuration.cs +++ b/Penumbra/Config/Configuration.cs @@ -7,6 +7,7 @@ using Penumbra.Api.Enums; using Penumbra.Files; using Penumbra.Import.Structs; +using Penumbra.Import.Textures; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.UI.Classes; @@ -108,6 +109,9 @@ public bool EnableMods [ConfigProperty(EventName = "ShowRenameChanged")] private RenameField _showRename = RenameField.BothDataPrio; + [ConfigProperty(EventName = "AuxiliaryDeviceModeChanged")] + private AuxiliaryDeviceMode _auxiliaryDeviceMode = AuxiliaryDeviceMode.Singleton; + public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Import/Textures/AuxiliaryDeviceMode.cs b/Penumbra/Import/Textures/AuxiliaryDeviceMode.cs new file mode 100644 index 000000000..95e4789c5 --- /dev/null +++ b/Penumbra/Import/Textures/AuxiliaryDeviceMode.cs @@ -0,0 +1,28 @@ +using Luna.Generators; + +namespace Penumbra.Import.Textures; + +/// Presented to the user as "Hardware Acceleration Mode for Texture Compression". +[NamedEnum(Utf16: false)] +[TooltipEnum] +public enum AuxiliaryDeviceMode +{ + [Name("Ephemeral")] + [Tooltip("Create an ephemeral Direct3D device object per texture compression operation.")] + Transient, + + [Name("Persistent")] + [Tooltip( + "Create a persistent Direct3D device object on the first texture compression operation and keep it until Penumbra is unloaded or when this setting is changed.")] + Singleton, + + [Name("Use Main Game Device")] + [Tooltip( + "Do not create an auxiliary Direct3D device object, and use the game's main one instead.\nWill cause freezes while doing texture compression operations.\nPrefer the above options if possible.")] + Borrowed, + + [Name("Disable Hardware Acceleration")] + [Tooltip( + "Do not create an auxiliary Direct3D device object, and use a software compression method instead.\nWill significantly slow down texture compression operations, and significantly degrade the output quality.\nONLY USE AS A LAST RESORT.")] + None, +} diff --git a/Penumbra/Import/Textures/SaveToDdsTexFileEffect.cs b/Penumbra/Import/Textures/SaveToDdsTexFileEffect.cs new file mode 100644 index 000000000..bde3a7252 --- /dev/null +++ b/Penumbra/Import/Textures/SaveToDdsTexFileEffect.cs @@ -0,0 +1,70 @@ +using Dalamud.Plugin.Services; +using Luna.DirectX; +using OtterTex; + +namespace Penumbra.Import.Textures; + +/// An image processing effect that saves its input to a file. +/// Dalamud's texture readback provider. +/// The texture manager service. +/// Whether to save as TEX. +/// The path to save at. +/// Which texture compression type to use. +/// Whether to add mipmaps to the saved texture. +public class SaveToDdsTexFileEffect( + ITextureReadbackProvider readbackProvider, + TextureManager textureManager, + bool asTex, + string path, + CombinedTexture.TextureSaveType textureSaveType = CombinedTexture.TextureSaveType.AsIs, + bool? mipMaps = null) : ScratchImageReadbackEffect(readbackProvider) +{ + /// Constructs a , inferring the format from the path's extension. + /// Dalamud's texture readback provider. + /// The texture manager service. + /// The path to save at. + /// Which texture compression type to use. + /// Whether to add mipmaps to the saved texture. + public SaveToDdsTexFileEffect(ITextureReadbackProvider readbackProvider, TextureManager textureManager, string path, + CombinedTexture.TextureSaveType textureSaveType = CombinedTexture.TextureSaveType.AsIs, + bool? mipMaps = null) + : this(readbackProvider, textureManager, IsTex(path), path, textureSaveType, mipMaps) + { } + + /// + protected override async Task Run(ScratchImage scratch, CancellationToken cancellationToken) + { + var converted = textureSaveType switch + { + CombinedTexture.TextureSaveType.AsIs => mipMaps is { } mips ? TextureManager.AddMipMaps(scratch, mips) : scratch, + CombinedTexture.TextureSaveType.Bitmap => TextureManager.CreateUncompressed(scratch, mipMaps ?? false, cancellationToken), + CombinedTexture.TextureSaveType.BC1 => await textureManager.CreateCompressedAsync(scratch, mipMaps ?? false, DXGIFormat.BC1UNorm, + cancellationToken), + CombinedTexture.TextureSaveType.BC3 => await textureManager.CreateCompressedAsync(scratch, mipMaps ?? false, DXGIFormat.BC3UNorm, + cancellationToken), + CombinedTexture.TextureSaveType.BC4 => await textureManager.CreateCompressedAsync(scratch, mipMaps ?? false, DXGIFormat.BC4UNorm, + cancellationToken), + CombinedTexture.TextureSaveType.BC5 => await textureManager.CreateCompressedAsync(scratch, mipMaps ?? false, DXGIFormat.BC5UNorm, + cancellationToken), + CombinedTexture.TextureSaveType.BC7 => await textureManager.CreateCompressedAsync(scratch, mipMaps ?? false, DXGIFormat.BC7UNorm, + cancellationToken), + _ => throw new Exception("Wrong save type."), + }; + + try + { + if (asTex) + TextureManager.SaveTex(path, converted); + else + converted.SaveDDS(path); + } + finally + { + if (converted != scratch) + converted.Dispose(); + } + } + + private static bool IsTex(string? path) + => Path.GetExtension(path)?.ToLowerInvariant() is ".tex" or ".atex"; +} diff --git a/Penumbra/Import/Textures/SaveToScratchImageEffect.cs b/Penumbra/Import/Textures/SaveToScratchImageEffect.cs new file mode 100644 index 000000000..d751d50aa --- /dev/null +++ b/Penumbra/Import/Textures/SaveToScratchImageEffect.cs @@ -0,0 +1,63 @@ +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using ImSharp; +using Luna.DirectX; +using OtterTex; +using Penumbra.Util; + +namespace Penumbra.Import.Textures; + +/// An image processing effect that saves its input to a . +/// Dalamud's texture readback provider. +/// +public abstract class SaveToScratchImageEffect(ITextureReadbackProvider readbackProvider) : WrapEffectBase, IDisposable +{ + private ScratchImage? _scratch; + + /// The last run's image. + public ScratchImage? ScratchImage + => _scratch; + + /// + public override int Count + => 0; + + /// + public override ImTextureId this[int index] + => throw new NotSupportedException(); + + ~SaveToScratchImageEffect() + => Dispose(false); + + /// Gets the last run's image, transferring its ownership to the caller. + public ScratchImage? DetachScratchImage() + { + var scratch = _scratch; + _scratch = null; + return scratch; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Releases the resources used by this object. + /// True if called explicitly, false if garbage collected. + protected virtual void Dispose(bool disposing) + { + _scratch?.Dispose(); + _scratch = null; + } + + /// + protected override async Task Run(IDalamudTextureWrap wrap, CancellationToken cancellationToken) + { + var scratch = await readbackProvider.GetScratchImageAsync(wrap, true, cancellationToken) + .ConfigureAwait(false); + _scratch?.Dispose(); + _scratch = scratch; + } +} diff --git a/Penumbra/Import/Textures/ScratchImageReadbackEffect.cs b/Penumbra/Import/Textures/ScratchImageReadbackEffect.cs new file mode 100644 index 000000000..9a12f44a0 --- /dev/null +++ b/Penumbra/Import/Textures/ScratchImageReadbackEffect.cs @@ -0,0 +1,37 @@ +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using ImSharp; +using Luna.DirectX; +using OtterTex; +using Penumbra.Util; + +namespace Penumbra.Import.Textures; + +/// Base class for effects that retrieve the pixel data as a and process it on the CPU side. +/// Dalamud's texture readback provider. +/// +public abstract class ScratchImageReadbackEffect(ITextureReadbackProvider readbackProvider) : WrapEffectBase +{ + /// + public override int Count + => 0; + + /// + public override ImTextureId this[int index] + => throw new NotSupportedException(); + + /// + protected override async Task Run(IDalamudTextureWrap wrap, CancellationToken cancellationToken) + { + using var scratch = await readbackProvider.GetScratchImageAsync(wrap, true, cancellationToken) + .ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + await Run(scratch, cancellationToken).ConfigureAwait(false); + } + + /// Runs this effect. + /// The input texture. + /// A cancellation token. + /// A task that represents this effect running. + protected abstract Task Run(ScratchImage scratch, CancellationToken cancellationToken); +} diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index f1338c9eb..22145d93d 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -247,9 +247,9 @@ public static TexFile.TextureFormat ToTexFormat(this DXGIFormat format) DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, - DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina + DXGIFormat.BC4UNorm => TexFile.TextureFormat.BC4, DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, - DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina + DXGIFormat.BC6HSF16 => TexFile.TextureFormat.BC6H, DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, @@ -274,9 +274,9 @@ public static DXGIFormat ToDXGI(this TexFile.TextureFormat format) TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, - (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina + TexFile.TextureFormat.BC4 => DXGIFormat.BC4UNorm, TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, - (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina + TexFile.TextureFormat.BC6H => DXGIFormat.BC6HSF16, TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index ce66922df..1e42688f4 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -2,15 +2,23 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; +using ImSharp; using Lumina.Data.Files; using Luna; +using Luna.DirectX; using OtterTex; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; namespace Penumbra.Import.Textures; -public sealed class TextureManager(IDataManager gameData, LunaLogger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder) +public sealed class TextureManager( + IDataManager gameData, + LunaLogger logger, + ITextureProvider textureProvider, + IUiBuilder uiBuilder, + IFramework framework, + Configuration configuration) : SingleTaskQueue, IDisposable, IService { private readonly LunaLogger _logger = logger; @@ -19,6 +27,8 @@ public sealed class TextureManager(IDataManager gameData, LunaLogger logger, ITe private readonly ConcurrentDictionary _tasks = new(); private bool _disposed; + private ComPtr _auxDevice; + public IReadOnlyDictionary Tasks => _tasks; @@ -28,6 +38,11 @@ public void Dispose() foreach (var (_, cancel) in _tasks.Values.ToArray()) cancel.Cancel(); _tasks.Clear(); + if (_auxDevice.Valid) + { + _auxDevice.Dispose(); + configuration.AuxiliaryDeviceModeChanged -= OnAuxiliaryDeviceModeChanged; + } } public Task SavePng(string input, string output) @@ -69,6 +84,9 @@ public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTe int width = 0, int height = 0) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, output, rgba, width, height)); + public Task RunEffectGraph(EffectGraph graph) + => Enqueue(new RunEffectGraphAction(graph, framework)); + private Task Enqueue(IAction action) { if (_disposed) @@ -305,6 +323,17 @@ public override int GetHashCode() => HashCode.Combine(_outputPath?.ToLowerInvariant(), _outputStream, _type, _mipMaps, _asTex, _input); } + private sealed class RunEffectGraphAction(EffectGraph graph, IFramework framework) : IAction + { + private readonly EffectGraph _graph = graph; + + public bool Equals(IAction? other) + => other is RunEffectGraphAction rhs && _graph == rhs._graph; + + public void Execute(CancellationToken token) + => _graph.Run(framework, token).Wait(token); + } + /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(BaseImage image, byte[]? rgba = null, int width = 0, int height = 0) { @@ -476,8 +505,56 @@ public static ScratchImage CreateUncompressed(ScratchImage input, bool mipMaps, return AddMipMaps(input, mipMaps); } + private void OnAuxiliaryDeviceModeChanged(AuxiliaryDeviceMode newMode, AuxiliaryDeviceMode oldMode) + { + if (_auxDevice.Valid && newMode is not AuxiliaryDeviceMode.Singleton) + { + _auxDevice.Dispose(); + configuration.AuxiliaryDeviceModeChanged -= OnAuxiliaryDeviceModeChanged; + } + } + + private unsafe ComPtr GetAuxiliaryDevice() + { + switch (configuration.AuxiliaryDeviceMode) + { + case AuxiliaryDeviceMode.Transient: + var clone = new ComPtr(); + CloneDevice((ID3D11Device*)uiBuilder.DeviceHandle, clone.GetAddressOf()); + return clone; + case AuxiliaryDeviceMode.Singleton: + if (!_auxDevice.Valid) + { + configuration.AuxiliaryDeviceModeChanged += OnAuxiliaryDeviceModeChanged; + CloneDevice((ID3D11Device*)uiBuilder.DeviceHandle, _auxDevice.GetAddressOf()); + } + + return new ComPtr(_auxDevice); + case AuxiliaryDeviceMode.Borrowed: return new ComPtr((ID3D11Device*)uiBuilder.DeviceHandle); + default: return default; + } + } + + private unsafe bool CanUseAuxiliaryDeviceImmediately() + => configuration.AuxiliaryDeviceMode is not AuxiliaryDeviceMode.Borrowed + || framework.IsInFrameworkUpdateThread && Im.Context.Pointer->WithinFrameScope; + + private static unsafe void CloneDevice(ID3D11Device* device, ID3D11Device** clone) + { + using var adapter = new ComPtr(); + using (var dxgiDevice = new ComPtr()) + { + Marshal.ThrowExceptionForHR(DxUtility.NonOwningComPtr(device).As(&dxgiDevice)); + Marshal.ThrowExceptionForHR(dxgiDevice.Get()->GetAdapter(adapter.GetAddressOf())); + } + + var featureLevel = device->GetFeatureLevel(); + Marshal.ThrowExceptionForHR(DirectX.D3D11CreateDevice(adapter, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, HMODULE.NULL, + device->GetCreationFlags(), &featureLevel, 1, D3D11.D3D11_SDK_VERSION, clone, null, null)); + } + /// 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 unsafe ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) + public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) { if (input.Meta.Format == format) return input; @@ -493,61 +570,79 @@ public unsafe ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DX // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { - ref var device = ref *(ID3D11Device*)uiBuilder.DeviceHandle; - IDXGIDevice* dxgiDevice; - Marshal.ThrowExceptionForHR(device.QueryInterface(TerraFX.Interop.Windows.Windows.__uuidof(), (void**)&dxgiDevice)); + var immediate = CanUseAuxiliaryDeviceImmediately(); + if (!immediate && framework.IsInFrameworkUpdateThread) + throw new InvalidOperationException("TextureManager's auxiliary Direct3D device is not ready"); + using var auxDevice = GetAuxiliaryDevice(); + if (auxDevice.Valid) + { + return immediate + ? Compress(input, auxDevice, format, CompressFlags.Parallel) + : CompressAsync(input, auxDevice, format, CompressFlags.Parallel).Result; + } + } + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); + } + + public Task CreateCompressedAsync(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) + { + if (input.Meta.Format == format) + return Task.FromResult(input); + + if (input.Meta.Format.IsCompressed()) + { + input = input.Decompress(DXGIFormat.B8G8R8A8UNorm); + cancel.ThrowIfCancellationRequested(); + } + + input = AddMipMaps(input, mipMaps); + cancel.ThrowIfCancellationRequested(); + // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. + if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + { + using var auxDevice = GetAuxiliaryDevice(); + if (auxDevice.Valid) + { + return CanUseAuxiliaryDeviceImmediately() + ? Task.FromResult(Compress(input, auxDevice, format, CompressFlags.Parallel)) + : CompressAsync(input, auxDevice, format, CompressFlags.Parallel); + } + } + + return Task.FromResult(input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel)); + } + + private static unsafe ScratchImage Compress(ScratchImage input, ComPtr device, DXGIFormat format, + CompressFlags compressFlags = CompressFlags.Default, float alphaWeight = 1.0f) + => input.Compress((nint)device.Get(), format, compressFlags, alphaWeight); + + private Task CompressAsync(ScratchImage input, ComPtr device, DXGIFormat format, + CompressFlags compressFlags = CompressFlags.Default, float alphaWeight = 1.0f) + { + var deviceAsync = new ComPtr(device); + var tcs = new TaskCompletionSource(); + Action action = null!; + + action = () => + { + uiBuilder.Draw -= action; try { - IDXGIAdapter* adapter = null; - Marshal.ThrowExceptionForHR(dxgiDevice->GetAdapter(&adapter)); - try - { - dxgiDevice->Release(); - dxgiDevice = null; - - ID3D11Device* deviceClone = null; - ID3D11DeviceContext* contextClone = null; - var featureLevel = device.GetFeatureLevel(); - Marshal.ThrowExceptionForHR(DirectX.D3D11CreateDevice( - adapter, - D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, - HMODULE.NULL, - device.GetCreationFlags(), - &featureLevel, - 1, - D3D11.D3D11_SDK_VERSION, - &deviceClone, - null, - &contextClone)); - try - { - adapter->Release(); - adapter = null; - return input.Compress((nint)deviceClone, format, CompressFlags.Parallel); - } - finally - { - if (contextClone is not null) - contextClone->Release(); - if (deviceClone is not null) - deviceClone->Release(); - } - } - finally - { - if (adapter is not null) - adapter->Release(); - } + tcs.SetResult(Compress(input, deviceAsync, format, compressFlags, alphaWeight)); + } + catch (Exception e) + { + tcs.SetException(e); } finally { - if (dxgiDevice is not null) - dxgiDevice->Release(); + deviceAsync.Dispose(); } - } + }; - return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); + uiBuilder.Draw += action; + return tcs.Task; } diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 970a8fd90..6f6a96b14 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -2,10 +2,10 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Luna; using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.SafeHandles; using Penumbra.String; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -62,7 +62,7 @@ private void SetupHssReplacements(CharacterBase* drawObject, uint slotIndex, Spa if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index e724ebc74..d27fc5b6b 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Luna; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 1700b980a..df5b87a91 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -2,9 +2,9 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Luna; using Penumbra.Api.Enums; using Penumbra.GameData; -using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index bec0831af..2420e2b08 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,8 +1,8 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Luna; using Penumbra.GameData.Interop; -using Penumbra.Interop.SafeHandles; namespace Penumbra.Interop.MaterialPreview; @@ -34,9 +34,9 @@ public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, Mate throw new InvalidOperationException("Draw object doesn't have color table textures"); _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); - - _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); + + _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture); if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs deleted file mode 100644 index a5e73867d..000000000 --- a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs +++ /dev/null @@ -1,41 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; - -namespace Penumbra.Interop.SafeHandles; - -public unsafe class SafeResourceHandle : SafeHandle, ICloneable -{ - public ResourceHandle* ResourceHandle - => (ResourceHandle*)handle; - - public override bool IsInvalid - => handle == 0; - - public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) - : base(0, ownsHandle) - { - if (incRef && !ownsHandle) - throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); - - if (incRef && handle != null) - handle->IncRef(); - SetHandle((nint)handle); - } - - public SafeResourceHandle Clone() - => new(ResourceHandle, true); - - object ICloneable.Clone() - => Clone(); - - public static SafeResourceHandle CreateInvalid() - => new(null, false); - - protected override bool ReleaseHandle() - { - var handle = Interlocked.Exchange(ref this.handle, 0); - if (handle != 0) - ((ResourceHandle*)handle)->DecRef(); - - return true; - } -} diff --git a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs b/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs deleted file mode 100644 index fd0208047..000000000 --- a/Penumbra/Interop/SafeHandles/SafeTextureHandle.cs +++ /dev/null @@ -1,49 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; - -namespace Penumbra.Interop.SafeHandles; - -public unsafe class SafeTextureHandle : SafeHandle -{ - public Texture* Texture - => (Texture*)handle; - - public override bool IsInvalid - => handle == 0; - - public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) - : base(0, ownsHandle) - { - if (incRef && !ownsHandle) - throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); - - if (incRef && handle != null) - handle->IncRef(); - SetHandle((nint)handle); - } - - public void Exchange(ref nint ppTexture) - { - lock (this) - { - handle = Interlocked.Exchange(ref ppTexture, handle); - } - } - - public static SafeTextureHandle CreateInvalid() - => new(null, false); - - protected override bool ReleaseHandle() - { - nint handle; - lock (this) - { - handle = this.handle; - this.handle = 0; - } - - if (handle != 0) - ((Texture*)handle)->DecRef(); - - return true; - } -} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index c7146ae6d..9c33d9fa0 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -91,6 +91,7 @@ public sealed class DebugTab : Window, ITab private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly LunaDxTester _lunaDxTester; private readonly HookOverrideDrawer _hookOverrides; private readonly RsfService _rsfService; private readonly ActionTmbListDrawer _actionTmbs; @@ -108,7 +109,7 @@ public DebugTab(Configuration config, CollectionManager collectionManager, Objec CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, EmoteListDrawer emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, + LunaDxTester lunaDxTester, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, ActionTmbListDrawer actionTmbs, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer, DragDropManager dragDropManager) @@ -146,6 +147,7 @@ public DebugTab(Configuration config, CollectionManager collectionManager, Objec _ipcTester = ipcTester; _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; + _lunaDxTester = lunaDxTester; _hookOverrides = hookOverrides; _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; @@ -190,6 +192,7 @@ public void DrawContent() DrawActorsDebug(); DrawCollectionCaches(); _texHeaderDrawer.Draw(); + _lunaDxTester.Draw(); _modMigratorDebug.Draw(); DrawShaderReplacementFixer(); DrawData(); diff --git a/Penumbra/UI/Tabs/Debug/LunaDxTester.cs b/Penumbra/UI/Tabs/Debug/LunaDxTester.cs new file mode 100644 index 000000000..c166d7d5b --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/LunaDxTester.cs @@ -0,0 +1,705 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using ImSharp; +using Lumina.Data.Files; +using Luna; +using Luna.DirectX; +using Luna.Generators; +using Penumbra.Import.Textures; +using Penumbra.UI.Classes; +using TerraFX.Interop.DirectX; + +namespace Penumbra.UI.Tabs.Debug; + +public class LunaDxTester( + TextureManager textureManager, + ITextureProvider textureProvider, + ITextureReadbackProvider readbackProvider, + IDataManager gameData, + FileDialogService fileDialogService) : IUiService +{ + [NamedEnum(Utf16: false)] + public enum GraphPreset + { + [Name("Resample (Scale)")] + ResampleScale, + + [Name("Resample (Fixed Size)")] + ResampleFixed, + + [Name("RGBA Blend")] + Blend4, + + [Name("RGB Composite")] + Composite, + + [Name("RGBA Composite with Control Mask")] + CompositeControlled, + + [Name("Extract Red")] + ExtractRed, + + [Name("Extract Green")] + ExtractGreen, + + [Name("Extract Blue")] + ExtractBlue, + + [Name("Extract Alpha")] + ExtractAlpha, + + [Name("Grayscale")] + Grayscale, + + [Name("Apply Index Map")] + ApplyIndex, + + [Name("Dual Kawase Blur")] + KawaseBlur, + + [Name("Refraction Raycast")] + RefractionRaycast, + + [Name("Export Dye Gloss Overlay")] + DyeGlossOverlay, + + [Name("Bring Your Own Pixel Shader")] + Custom, + + [Name("Bring Your Own Compute Shader")] + CustomCompute, + } + + // Graph preset. + private GraphPreset _graphPreset; + + // Input/output paths. + private string _inputPath = string.Empty; + private string _input2Path = string.Empty; + private string _input3Path = string.Empty; + private string _input4Path = string.Empty; + private string _shaderPath = string.Empty; + private string _outputPath = string.Empty; + + // Save options. + private CombinedTexture.TextureSaveType _saveType = CombinedTexture.TextureSaveType.AsIs; + private bool _saveWithMips = true; + + // Resample options. + private float _optScale = 1.0f; + private int _optWidth = 32; + private int _optHeight = 32; + private LunaEffects.ResampleMethod _optResample = LunaEffects.ResampleMethod.Bilinear; + + // Blend/Composite options. + private LunaShaders.CompositeFunction _optComposite = LunaShaders.CompositeFunction.Over; + private LunaShaders.Blend _optBlend = LunaShaders.Blend.Source; + private Vector4 _optFgXform = new(1.0f, 0.0f, 0.0f, 1.0f); + private Vector2 _optFgOffset = Vector2.Zero; + private Vector4 _optBgCtl = new(0.2126f, 0.7152f, 0.0722f, 0.0f); + private Vector4 _optFgCtl = new(0.2126f, 0.7152f, 0.0722f, 0.0f); + private float _optBgCtl0 = 0.0f; + private float _optFgCtl0 = 0.0f; + private Vector4 _optLerp = new(1.0f, 0.0f, 1.0f, 0.0f); + + // Apply Index Map options. + private LunaShaders.Palette _optPalette = new(); + + // Blur options. + private Vector4 _optRounding = new(8.0f); + private Vector2 _optUvMin = Vector2.Zero; + private Vector2 _optUvMax = Vector2.One; + private float _optBlur = 1.0f; + private float _optOpacity = 1.0f; + + // Refraction Raycast options. + private float _optDepth = 1.0f; + private float _optIor = 1.33f; + private bool _optNn = false; + + // Bring Your Own Compute Shader options. + private int _optTgcX = D3D11.D3D11_CS_THREAD_GROUP_MIN_X; + private int _optTgcY = D3D11.D3D11_CS_THREAD_GROUP_MIN_Y; + private int _optTgcZ = D3D11.D3D11_CS_THREAD_GROUP_MIN_Z; + + // Results. + private EffectGraph? _lastGraph; + private TextureStandIn _lastOutput; + private Task _lastRun = Task.CompletedTask; + + public void Draw() + { + using var header = Im.Tree.HeaderId("Luna.DirectX Tester"u8); + if (!header) + return; + + DrawGraphPreset(); + DrawInputPath(); + DrawShaderPath(); + DrawGraphOptions(); + DrawOutputPath(); + DrawOutputOptions(); + DrawActions(); + DrawResults(); + } + + private void DrawGraphPreset() + => Im.Combo.DrawEnum("Effect Graph Preset"u8, ref _graphPreset, static i => i.ToNameU8()); + + private void DrawInputPath() + { + if (_graphPreset is GraphPreset.DyeGlossOverlay) + return; + + switch (_graphPreset) + { + case GraphPreset.Blend4 or GraphPreset.Composite: + DrawInputPath("Background Input Path"u8, "Luna.DirectX Tester: Select Background Input", _inputPath, + value => _inputPath = value); + DrawInputPath("Foreground Input Path"u8, "Luna.DirectX Tester: Select Foreground Input", _input2Path, + value => _input2Path = value); + break; + case GraphPreset.CompositeControlled: + DrawInputPath("Background Input Path"u8, "Luna.DirectX Tester: Select Background Input", _inputPath, + value => _inputPath = value); + DrawInputPath("Background Control Mask Path"u8, "Luna.DirectX Tester: Select Background Control Mask", _input3Path, + value => _input3Path = value); + DrawInputPath("Foreground Input Path"u8, "Luna.DirectX Tester: Select Foreground Input", _input2Path, + value => _input2Path = value); + DrawInputPath("Foreground Control Mask Path"u8, "Luna.DirectX Tester: Select Foreground Control Mask", _input4Path, + value => _input4Path = value); + break; + default: DrawInputPath("Input Path"u8, "Luna.DirectX Tester: Select Input", _inputPath, value => _inputPath = value); break; + } + } + + private void DrawShaderPath() + { + if (_graphPreset is not GraphPreset.Custom and not GraphPreset.CustomCompute) + return; + + using var id = Im.Id.Push("Shader Path"u8); + + DrawInputPath("Shader Path"u8, "Luna.DirectX Tester: Select Shader", _shaderPath, value => _shaderPath = value); + } + + private void DrawInputPath(ReadOnlySpan label, string pickerTitle, string value, Action setter) + { + using var id = Im.Id.Push(label); + + if (Im.Input.Text(label, ref value)) + setter(value); + Im.Line.SameInner(); + if (ImEx.Icon.Button(LunaStyle.FolderIcon)) + fileDialogService.OpenFilePicker(pickerTitle, ".*", (ok, files) => + { + if (ok && files.Count > 0) + setter(files[0]); + }, 1, null, false); + } + + private void DrawOutputPath() + { + using var id = Im.Id.Push("Output Path"u8); + + Im.Input.Text("Output Path"u8, ref _outputPath); + Im.Line.SameInner(); + if (ImEx.Icon.Button(LunaStyle.FolderIcon)) + fileDialogService.OpenSavePicker("Luna.DirectX Tester: Select Output", ".*", "texture.dds", ".dds", (ok, file) => + { + if (ok) + _outputPath = file; + }, null, false); + } + + private void DrawOutputOptions() + { + if (Path.GetExtension(_outputPath).ToLowerInvariant() is not ".dds" and not ".tex" and not ".atex") + return; + + Im.Combo.DrawEnum("Compression"u8, ref _saveType); + if (_saveType is not CombinedTexture.TextureSaveType.AsIs) + Im.Checkbox("Generate Mipmaps"u8, ref _saveWithMips); + } + + private void DrawGraphOptions() + { + switch (_graphPreset) + { + case GraphPreset.ResampleScale: + Im.Drag("Scale"u8, ref _optScale); + Im.Combo.DrawEnum("Resampling Method"u8, ref _optResample, static resample => resample.ToNameU8()); + break; + case GraphPreset.ResampleFixed: + DrawDimensions(); + Im.Combo.DrawEnum("Resampling Method"u8, ref _optResample, static resample => resample.ToNameU8()); + break; + case GraphPreset.Blend4: + Im.DragN("Foreground UV Transform"u8, AsFloats(ref _optFgXform)); + Im.DragN("Foreground UV Offset"u8, AsFloats(ref _optFgOffset)); + Im.Combo.DrawEnum("Foreground Resampling Method"u8, ref _optResample, static resample => resample.ToNameU8()); + DrawBlendOptions(); + break; + case GraphPreset.Composite: + Im.DragN("Foreground UV Transform"u8, AsFloats(ref _optFgXform)); + Im.DragN("Foreground UV Offset"u8, AsFloats(ref _optFgOffset)); + Im.Combo.DrawEnum("Foreground Resampling Method"u8, ref _optResample, static resample => resample.ToNameU8()); + Im.Combo.DrawEnum("Compositing Function"u8, ref _optComposite, static composite => composite.ToNameU8()); + DrawBlendOptions(); + break; + case GraphPreset.CompositeControlled: + Im.DragN("Foreground UV Transform"u8, AsFloats(ref _optFgXform)); + Im.DragN("Foreground UV Offset"u8, AsFloats(ref _optFgOffset)); + Im.Combo.DrawEnum("Foreground Resampling Method"u8, ref _optResample, static resample => resample.ToNameU8()); + Im.Combo.DrawEnum("Compositing Function"u8, ref _optComposite, static composite => composite.ToNameU8()); + Im.DragN("Background Control Weights"u8, AsFloats(ref _optBgCtl)); + Im.Drag("Background Control Bias"u8, ref _optBgCtl0); + Im.DragN("Foreground Control Weights"u8, AsFloats(ref _optFgCtl)); + Im.Drag("Foreground Control Bias"u8, ref _optFgCtl0); + DrawBlendOptions(); + break; + case GraphPreset.ApplyIndex: + for (var i = 0; i < LunaShaders.Palette.Length; ++i) + Im.Color.Editor($"Palette Color {(i >> 1) + 1}{((i & 1) is not 0 ? 'B' : 'A')}", ref _optPalette[i], + ColorEditorFlags.AlphaPreviewHalf + | ColorEditorFlags.Float + | ColorEditorFlags.Hdr + | ColorEditorFlags.AlphaBar + | ColorEditorFlags.DisplayRgb + | ColorEditorFlags.InputRgb); + break; + case GraphPreset.KawaseBlur: + Im.DragN("Blurred Rectangle Top-Left UV"u8, AsFloats(ref _optUvMin)); + Im.DragN("Blurred Rectangle Bottom-Right UV"u8, AsFloats(ref _optUvMax)); + Im.DragN("Blurred Rectangle Corner Rounding Radii"u8, AsFloats(ref _optRounding)); + Im.Drag("Blur Strength"u8, ref _optBlur); + Im.Drag("Opacity of Unblurred Regions"u8, ref _optOpacity); + break; + case GraphPreset.RefractionRaycast: + Im.Drag("Depth"u8, ref _optDepth); + Im.Drag("Index of Refraction"u8, ref _optIor); + break; + case GraphPreset.DyeGlossOverlay: + DrawDimensions(); + Im.DragN("Corner Rounding Radii"u8, AsFloats(ref _optRounding)); + break; + case GraphPreset.Custom: + Im.Drag("Scale"u8, ref _optScale); + Im.Checkbox("Use Nearest-Neighbor Sampling"u8, ref _optNn); + break; + case GraphPreset.CustomCompute: + DrawDimensions(); + DrawThreadGroupCount(); + Im.Checkbox("Use Nearest-Neighbor Sampling"u8, ref _optNn); + break; + } + } + + private bool DrawDimensions() + { + Span dimensions = stackalloc int[2]; + dimensions[0] = _optWidth; + dimensions[1] = _optHeight; + if (!Im.DragN("Dimensions"u8, dimensions)) + return false; + + _optWidth = dimensions[0]; + _optHeight = dimensions[1]; + return true; + } + + private void DrawBlendOptions() + { + var blend = (uint)(_optBlend & ~LunaShaders.Blend.SwapInputs); + if (Im.Combo.DrawItems("Blend Function"u8, ref blend, ComboFlags.None, static blend => ((LunaShaders.Blend)blend).ToNameU8(), + LunaShaders.Blend.WellKnownFunctions.Select(static blend => (uint)blend))) + _optBlend = (_optBlend & LunaShaders.Blend.SwapInputs) | (LunaShaders.Blend)blend; + if (!_optBlend.Commutative) + { + Im.Line.Same(); + var blendSwap = _optBlend.HasFlag(LunaShaders.Blend.SwapInputs); + if (Im.Checkbox("Swap Inputs"u8, ref blendSwap)) + _optBlend = blendSwap ? _optBlend | LunaShaders.Blend.SwapInputs : _optBlend & ~LunaShaders.Blend.SwapInputs; + } + + switch (_optBlend & LunaShaders.Blend.FunctionMask) + { + case LunaShaders.Blend.Lerp: Im.DragN("Interpolation Weights"u8, AsFloats(ref _optLerp)); break; + } + } + + private bool DrawThreadGroupCount() + { + Span tgc = stackalloc int[3]; + tgc[0] = _optTgcX; + tgc[1] = _optTgcY; + tgc[2] = _optTgcZ; + if (!Im.DragN("Thread Group Count"u8, tgc)) + return false; + + _optTgcX = Math.Clamp(tgc[0], D3D11.D3D11_CS_THREAD_GROUP_MIN_X, D3D11.D3D11_CS_THREAD_GROUP_MAX_X); + _optTgcY = Math.Clamp(tgc[1], D3D11.D3D11_CS_THREAD_GROUP_MIN_Y, D3D11.D3D11_CS_THREAD_GROUP_MAX_Y); + _optTgcZ = Math.Clamp(tgc[2], D3D11.D3D11_CS_THREAD_GROUP_MIN_Z, D3D11.D3D11_CS_THREAD_GROUP_MAX_Z); + return true; + } + + private void DrawActions() + { + using var disabled = Im.Disabled(!_lastRun.IsCompleted); + + if (ImEx.Icon.LabeledButton(FontAwesomeIcon.Play.Icon(), "Run"u8)) + { + _lastGraph?.Dispose(); + try + { + (_lastGraph, _lastOutput) = BuildGraph(); + } + catch (Exception e) + { + _lastGraph = null; + _lastOutput = default; + _lastRun = Task.FromException(e); + } + + if (_lastGraph is not null) + _lastRun = textureManager.RunEffectGraph(_lastGraph); + } + + Im.Line.Same(); + using (Im.Disabled(_lastGraph is null)) + { + if (Im.Button("Re-run"u8) && _lastGraph is not null) + _lastRun = textureManager.RunEffectGraph(_lastGraph); + } + + Im.Tooltip.OnHover( + "This will re-run the effect graph as it was last built by the \"Run\" action, without taking into account any changes in the settings.\nThis action is only here to test the Luna.DirectX framework itself."u8); + + Im.Line.Same(); + if (Im.Button("Clear"u8)) + { + _lastOutput = default; + _lastGraph?.Dispose(); + _lastGraph = null; + _lastRun = Task.CompletedTask; + } + } + + private void DrawResults() + { + if (!_lastRun.IsCompleted) + { + Im.Text("Cooking..."u8); + return; + } + + if (!_lastRun.IsCompletedSuccessfully) + { + using var color = ImGuiColor.Text.Push(ImGuiColors.ErrorForeground); + Im.Text(_lastRun.Exception?.ToString() ?? "An unspecified error occurred."); + return; + } + + var output = _lastOutput.Id; + if (output.IsNull) + return; + + var dimensions = output.Dimensions; + var imageSize = new Vector2(dimensions.Width, dimensions.Height); + var iconSize = imageSize; + if (iconSize.X > 320.0f || iconSize.Y > 320.0f) + iconSize *= 320.0f / MathF.Max(iconSize.X, iconSize.Y); + Im.Image.DrawScaled(output, iconSize, imageSize); + } + + private (EffectGraph, TextureStandIn) BuildGraph() + { + switch (_graphPreset) + { + case GraphPreset.ResampleScale: + { + var input = BuildLoadInput(); + var resize = BuildResampleScale(input); + var output = BuildSaveOutput(resize); + return ([input, resize, output], new TextureStandIn(resize, 0)); + } + case GraphPreset.ResampleFixed: + { + var input = BuildLoadInput(); + var resize = BuildResampleFixed(input); + var output = BuildSaveOutput(resize); + return ([input, resize, output], new TextureStandIn(resize, 0)); + } + case GraphPreset.Blend4: + { + var (input1, input2) = BuildLoadInputs2(); + var blend = BuildBlend4(input1, input2); + var output = BuildSaveOutput(blend); + return ([input1, input2, blend, output], new TextureStandIn(blend, 0)); + } + case GraphPreset.Composite: + { + var (input1, input2) = BuildLoadInputs2(); + var composite = BuildComposite(input1, input2); + var output = BuildSaveOutput(composite); + return ([input1, input2, composite, output], new TextureStandIn(composite, 0)); + } + case GraphPreset.CompositeControlled: + { + var (input1, input2, input3, input4) = BuildLoadInputs4(); + var composite = BuildCompositeControlled(input1, input2, input3, input4); + var output = BuildSaveOutput(composite); + return ([input1, input2, composite, output], new TextureStandIn(composite, 0)); + } + case >= GraphPreset.ExtractRed and <= GraphPreset.Grayscale: + { + var input = BuildLoadInput(); + var transform = BuildColorTransform(input); + var output = BuildSaveOutput(transform); + return ([input, transform, output], new TextureStandIn(transform, 0)); + } + case GraphPreset.ApplyIndex: + { + var input = BuildLoadInput(); + var result = BuildApplyIndex(input); + var output = BuildSaveOutput(result); + return ([input, result, output], new TextureStandIn(result, 0)); + } + case GraphPreset.KawaseBlur: + { + var input = BuildLoadInput(); + var blur = BuildKawaseBlur(input); + var output = BuildSaveOutput(blur); + return ([input, blur, output], new TextureStandIn(blur, 0)); + } + case GraphPreset.RefractionRaycast: + { + var input = BuildLoadInput(); + var projection = BuildRefractionRaycast(input); + var output = BuildSaveOutput(projection); + return ([input, projection, output], new TextureStandIn(projection, 0)); + } + case GraphPreset.DyeGlossOverlay: + { + var generate = BuildDyeGlossOverlay(); + var output = BuildSaveOutput(generate); + return ([generate, output], new TextureStandIn(generate, 0)); + } + case GraphPreset.Custom: + { + var input = BuildLoadInput(); + var filter = BuildCustom(input); + var output = BuildSaveOutput(filter); + return ([input, filter, output], new TextureStandIn(filter, 0)); + } + case GraphPreset.CustomCompute: + { + var input = BuildLoadInput(); + var filter = BuildCustomCompute(input); + var output = BuildSaveOutput(filter); + return ([input, filter, output], new TextureStandIn(filter, 0)); + } + default: throw new NotImplementedException(); + } + } + + private IEffect BuildLoadInput() + => BuildLoadInput(_inputPath); + + private (IEffect, IEffect) BuildLoadInputs2() + { + var input1 = BuildLoadInput(); + var input2 = string.Equals(_inputPath, _input2Path, StringComparison.Ordinal) ? input1 : BuildLoadInput(_input2Path); + return (input1, input2); + } + + private (IEffect, IEffect, IEffect) BuildLoadInputs3() + { + var (input1, input2) = BuildLoadInputs2(); + var input3 = string.Equals(_inputPath, _input3Path, StringComparison.Ordinal) ? input1 : + string.Equals(_input2Path, _input3Path, StringComparison.Ordinal) ? input2 : BuildLoadInput(_input3Path); + return (input1, input2, input3); + } + + private (IEffect, IEffect, IEffect, IEffect) BuildLoadInputs4() + { + var (input1, input2, input3) = BuildLoadInputs3(); + var input4 = string.Equals(_inputPath, _input4Path, StringComparison.Ordinal) ? input1 : + string.Equals(_input2Path, _input4Path, StringComparison.Ordinal) ? input2 : + string.Equals(_input3Path, _input4Path, StringComparison.Ordinal) ? input3 : BuildLoadInput(_input4Path); + return (input1, input2, input3, input4); + } + + private IEffect BuildLoadInput(string path) + => Path.IsPathRooted(path) + ? new LoadEffect(textureProvider.CreateFromImageAsync(File.OpenRead(path))) + : new LoadEffect(LoadInputGameFile(path)); + + private async Task LoadInputGameFile(string path) + => await textureProvider.CreateFromTexFileAsync(await gameData.GetFileAsync(path, CancellationToken.None)); + + private IEffect BuildResampleScale(IEffect input) + => LunaEffects.Resample(new TextureStandIn(input, 0), _optScale, _optResample); + + private IEffect BuildResampleFixed(IEffect input) + => LunaEffects.Resample(new TextureStandIn(input, 0), (_optWidth, _optHeight), _optResample); + + private IEffect BuildBlend4(IEffect input, IEffect input2) + => LunaEffects.Blend4(new TextureStandIn(input, 0), new TextureStandIn(input2, 0), out _, new LunaShaders.Blend4Uniforms + { + ForegroundTransform = _optFgXform, + ForegroundOffset = _optFgOffset, + Blend = _optBlend, + BlendParameters = GetBlendParameters(), + }, + _optResample); + + private IEffect BuildComposite(IEffect input, IEffect input2) + => LunaEffects.Composite(new TextureStandIn(input, 0), new TextureStandIn(input2, 0), out _, new LunaShaders.CompositeUniforms + { + ForegroundTransform = _optFgXform, + ForegroundOffset = _optFgOffset, + Blend = _optBlend, + ColorCompositeWeights = _optComposite.Weights, + AlphaCompositeWeights = _optComposite.Weights, + BlendParameters = GetBlendParameters(), + }, + _optResample); + + private IEffect BuildCompositeControlled(IEffect input, IEffect input2, IEffect input3, IEffect input4) + => LunaEffects.CompositeControlled(new TextureStandIn(input, 0), new TextureStandIn(input3, 0), new TextureStandIn(input2, 0), + new TextureStandIn(input4, 0), out _, new LunaShaders.CompositeControlledUniforms + { + ForegroundTransform = _optFgXform, + ForegroundOffset = _optFgOffset, + Blend = _optBlend, + CompositeWeights = _optComposite.Weights, + BackgroundControl0 = _optBgCtl0, + ControlCompositeWeights = _optComposite.Weights, + ForegroundControl0 = _optFgCtl0, + BackgroundControlWeights = _optBgCtl, + ForegroundControlWeights = _optFgCtl, + BlendParameters = GetBlendParameters(), + }, + _optResample); + + private LunaShaders.BlendParameters GetBlendParameters() + => (_optBlend & LunaShaders.Blend.FunctionMask) switch + { + LunaShaders.Blend.Lerp => new LunaShaders.BlendParameters + { + LerpWeights = _optLerp, + }, + _ => default, + }; + + private IEffect BuildApplyIndex(IEffect input) + => LunaEffects.ApplyIndex(new TextureStandIn(input, 0), out _, new LunaShaders.ApplyIndexUniforms + { + Exponent = Vector4.One, + Palette = _optPalette, + }); + + private IEffect BuildKawaseBlur(IEffect input) + => LunaEffects.KawaseBlur(new TextureStandIn(input, 0), out _, new LunaShaders.KawaseUniforms + { + BlurRectRounding = _optRounding, + BlurRectUvMin = _optUvMin, + BlurRectUvMax = _optUvMax, + BlurStrength = _optBlur, + UnblurredOpacity = _optOpacity, + }); + + private IEffect BuildColorTransform(IEffect input) + => LunaEffects.ColorTransform(new TextureStandIn(input, 0), out _, new LunaShaders.ColorTransformUniforms + { + BasisRed = _graphPreset switch + { + GraphPreset.ExtractRed => Vector4.One - Vector4.UnitW, + GraphPreset.Grayscale => new Vector4(0.2126f, 0.2126f, 0.2126f, 0.0f), + _ => Vector4.Zero, + }, + BasisGreen = _graphPreset switch + { + GraphPreset.ExtractGreen => Vector4.One - Vector4.UnitW, + GraphPreset.Grayscale => new Vector4(0.7152f, 0.7152f, 0.7152f, 0.0f), + _ => Vector4.Zero, + }, + BasisBlue = _graphPreset switch + { + GraphPreset.ExtractBlue => Vector4.One - Vector4.UnitW, + GraphPreset.Grayscale => new Vector4(0.0722f, 0.0722f, 0.0722f, 0.0f), + _ => Vector4.Zero, + }, + BasisAlpha = _graphPreset switch + { + GraphPreset.ExtractAlpha => Vector4.One - Vector4.UnitW, + GraphPreset.Grayscale => Vector4.UnitW, + _ => Vector4.Zero, + }, + Origin = _graphPreset switch + { + GraphPreset.Grayscale => Vector4.Zero, + _ => Vector4.UnitW, + }, + }); + + private IEffect BuildRefractionRaycast(IEffect input) + => LunaEffects.RefractionRaycast(new TextureStandIn(input, 0), out _, new LunaShaders.RefractionRaycastUniforms + { + Depth = _optDepth, + IndexOfRefraction = _optIor, + }); + + private IEffect BuildSaveOutput(IEffect input) + => string.IsNullOrEmpty(_outputPath) + ? IEffect.Null + : Path.GetExtension(_outputPath).ToLowerInvariant() switch + { + ".dds" or ".tex" or ".atex" => new SaveToDdsTexFileEffect(readbackProvider, textureManager, _outputPath, _saveType, + _saveType is CombinedTexture.TextureSaveType.AsIs ? null : _saveWithMips) + { + Input = new TextureStandIn(input, 0), + }, + _ => new SaveToFileEffect(readbackProvider, _outputPath) + { + Input = new TextureStandIn(input, 0), + }, + }; + + private IEffect BuildDyeGlossOverlay() + => new ShaderFilterEffect(LunaShaders.DyeGlossOverlay, new ConstantBuffer( + new LunaShaders.DyeGlossOverlayUniforms + { + Rounding = _optRounding, + }), "Dye Gloss Overlay") + { + Dimensions = (_optWidth, _optHeight), + }; + + private IEffect BuildCustom(IEffect input) + { + var effect = new ShaderFilterEffect(new PixelShader(File.ReadAllBytes(_shaderPath), Path.GetFileName(_shaderPath)), null, + $"Bring Your Own Pixel Shader ({Path.GetFileName(_shaderPath)})"); + effect.DimensionsStrategy = ShaderFilterEffect.ScaleLargestInput(_optScale); + effect.Textures.Add(new TextureStandIn(input, 0)); + effect.Samplers.Add(_optNn ? Sampler.ClampNearestNeighbor : Sampler.ClampBilinear); + return effect; + } + + private IEffect BuildCustomCompute(IEffect input) + { + var effect = new ComputeFilterEffect(new ComputeShader(File.ReadAllBytes(_shaderPath), Path.GetFileName(_shaderPath)), null, + $"Bring Your Own Compute Shader ({Path.GetFileName(_shaderPath)})"); + effect.ThreadGroupCount = (_optTgcX, _optTgcY, _optTgcZ); + effect.Textures.Add(new TextureStandIn(input, 0)); + effect.Outputs.Add(new RwImage((_optWidth, _optHeight), FullScreenQuad.DefaultOutputFormat)); + effect.Samplers.Add(_optNn ? Sampler.ClampNearestNeighbor : Sampler.ClampBilinear); + return effect; + } + + private static Span AsFloats(ref T container) where T : unmanaged + => MemoryMarshal.Cast(new Span(ref container)); +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 26a6c8fb1..41959af6d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -6,6 +6,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Import.Textures; using Penumbra.Interop; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; @@ -857,6 +858,7 @@ private void DrawAdvancedSettings() DrawCrashHandler(); DrawMinimumDimensionConfig(); DrawHdrRenderTargets(); + DrawAuxiliaryDeviceMode(); Checkbox("Auto Deduplicate on Import"u8, "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files."u8, _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); @@ -1013,6 +1015,25 @@ private void DrawHdrRenderTargets() #pragma warning restore CS0162 // Unreachable code detected } + private void DrawAuxiliaryDeviceMode() + { + Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); + using (var combo = Im.Combo.Begin("##auxiliaryDeviceMode"u8, _config.AuxiliaryDeviceMode.ToNameU8())) + { + if (combo) + foreach (var value in AuxiliaryDeviceMode.Values) + { + if (Im.Selectable(value.ToNameU8(), _config.AuxiliaryDeviceMode == value)) + _config.AuxiliaryDeviceMode = value; + + Im.Tooltip.OnHover(value.Tooltip()); + } + } + + LunaStyle.DrawAlignedHelpMarkerLabel("Hardware Acceleration Mode for Texture Compression"u8, + "How to manage hardware acceleration for texture compression.\nChange this if you run into ReShade issues after compressing textures."u8); + } + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { diff --git a/Penumbra/Util/LunaDxExtensions.cs b/Penumbra/Util/LunaDxExtensions.cs new file mode 100644 index 000000000..6b145d08f --- /dev/null +++ b/Penumbra/Util/LunaDxExtensions.cs @@ -0,0 +1,76 @@ +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using Luna.DirectX; +using OtterTex; +using Penumbra.GameData.Files.MaterialStructs; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; +using LunaDxImage = Luna.DirectX.Image; +using OtterTexImage = OtterTex.Image; + +namespace Penumbra.Util; + +public static class LunaDxExtensions +{ + public static unsafe LunaDxImage ToDirectXImage(this ScratchImage scratch, D3D11_USAGE usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE, + D3D11_BIND_FLAG bind = D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE) + { + if (scratch.Meta.Dimension is not TexDimension.Tex2D || scratch.Meta.MipLevels is 0) + throw new ArgumentException("The given ScratchImage is not suitable for an upload to GPU as a 2D texture."); + + var desc = new D3D11_TEXTURE2D_DESC + { + Width = (uint)scratch.Meta.Width, + Height = (uint)scratch.Meta.Height, + MipLevels = (uint)scratch.Meta.MipLevels, + ArraySize = (uint)scratch.Meta.ArraySize, + Format = (DXGI_FORMAT)scratch.Meta.Format, + SampleDesc = new DXGI_SAMPLE_DESC(1, 0), + Usage = usage, + BindFlags = (uint)bind, + CPUAccessFlags = 0, + MiscFlags = (uint)scratch.Meta.MiscFlags, + }; + var contents = stackalloc D3D11_SUBRESOURCE_DATA[scratch.Images.Length]; + for (var i = 0; i < scratch.Images.Length; ++i) + scratch.Images[i].ToSubresourceData(out contents[i]); + + using var texture = new ComPtr(); + Marshal.ThrowExceptionForHR(CustomRenderManager.Instance.Device->CreateTexture2D(&desc, contents, texture.GetAddressOf())); + return new LunaDxImage(texture); + } + + public static async Task GetScratchImageAsync(this ITextureReadbackProvider readbackProvider, IDalamudTextureWrap wrap, + bool leaveWrapOpen = false, CancellationToken cancellationToken = default) + { + var (mipLevels, images) = await readbackProvider.GetAllRawImagesAsync(wrap, leaveWrapOpen, cancellationToken); + var spec0 = images[0].Specification; + var scratch = ScratchImage.Initialize2D((DXGIFormat)spec0.DxgiFormat, spec0.Width, spec0.Height, images.Length / mipLevels, mipLevels); + for (var i = 0; i < scratch.Images.Length; ++i) + images[i].RawData.CopyTo(scratch.Images[i].WritableSpan); + return scratch; + } + + public static Sampler CreateSampler(this SamplerFlags flags, bool bilinear = true) + => new(D3D11_SAMPLER_DESC.DEFAULT with + { + Filter = bilinear ? D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR : D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_POINT, + AddressU = (D3D11_TEXTURE_ADDRESS_MODE)((uint)flags.UAddressMode + 1), + AddressV = (D3D11_TEXTURE_ADDRESS_MODE)((uint)flags.VAddressMode + 1), + MinLOD = flags.MinLod, + MipLODBias = flags.LodBias, + }); + + extension(in OtterTexImage image) + { + private unsafe void ToSubresourceData(out D3D11_SUBRESOURCE_DATA subresData) + { + subresData.pSysMem = (void*)image.Pixels; + subresData.SysMemPitch = (uint)image.RowPitch; + subresData.SysMemSlicePitch = (uint)image.SlicePitch; + } + + private unsafe Span WritableSpan + => new((void*)image.Pixels, image.Span.Length); + } +}