diff --git a/Meddle/Meddle.Plugin/Lib/DirectXTexC.dll b/Meddle/Meddle.Plugin/Lib/DirectXTexC.dll new file mode 100644 index 0000000..2cab1dc Binary files /dev/null and b/Meddle/Meddle.Plugin/Lib/DirectXTexC.dll differ diff --git a/Meddle/Meddle.Plugin/Lib/OtterTex.dll b/Meddle/Meddle.Plugin/Lib/OtterTex.dll new file mode 100644 index 0000000..29912e6 Binary files /dev/null and b/Meddle/Meddle.Plugin/Lib/OtterTex.dll differ diff --git a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj index 861083a..eb59d59 100644 --- a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj +++ b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj @@ -22,9 +22,10 @@ - - - + + PreserveNewest + DirectXTexC.dll + @@ -49,6 +50,9 @@ $(DalamudLibPath)Newtonsoft.Json.dll False + + Lib\OtterTex.dll + diff --git a/Meddle/Meddle.Plugin/Service.cs b/Meddle/Meddle.Plugin/Service.cs index 6d6edc1..6658932 100644 --- a/Meddle/Meddle.Plugin/Service.cs +++ b/Meddle/Meddle.Plugin/Service.cs @@ -11,6 +11,7 @@ public class Service public Service(DalamudPluginInterface pluginInterface) { pluginInterface.Inject(this); + Log = PrivLog; } public void AddServices(IServiceCollection services) @@ -18,7 +19,7 @@ public void AddServices(IServiceCollection services) services.AddSingleton(PluginInterface); services.AddSingleton(Framework); services.AddSingleton(CommandManager); - services.AddSingleton(Log); + services.AddSingleton(PrivLog); services.AddSingleton(ObjectTable); services.AddSingleton(ClientState); services.AddSingleton(SigScanner); @@ -32,7 +33,9 @@ public void AddServices(IServiceCollection services) [PluginService] private ICommandManager CommandManager { get; set; } = null!; - [PluginService] private IPluginLog Log { get; set; } = null!; + public static IPluginLog Log { get; private set; } = null!; + + [PluginService] private IPluginLog PrivLog { get; set; } = null!; [PluginService] private IObjectTable ObjectTable { get; set; } = null!; [PluginService] private IClientState ClientState { get; set; } = null!; [PluginService] private ISigScanner SigScanner { get; set; } = null!; diff --git a/Meddle/Meddle.Plugin/UI/CharacterTab.Utils.cs b/Meddle/Meddle.Plugin/UI/CharacterTab.Utils.cs index 7ff3c3f..2ed57af 100644 --- a/Meddle/Meddle.Plugin/UI/CharacterTab.Utils.cs +++ b/Meddle/Meddle.Plugin/UI/CharacterTab.Utils.cs @@ -364,9 +364,10 @@ public static void DrawMaterial(Material material) ImGui.BulletText($"Shader: {material.ShaderPackage.Name}"); // Display Material Textures in the same table - using (var textable = ImRaii.Table("TextureTable", 2, ImGuiTableFlags.Borders)) + using (var textable = ImRaii.Table("TextureTable", 3, ImGuiTableFlags.Borders)) { ImGui.TableSetupColumn("Texture Usage", ImGuiTableColumnFlags.WidthFixed, 120); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 120); ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -376,6 +377,8 @@ public static void DrawMaterial(Material material) ImGui.TableNextColumn(); ImGui.Text($"{texture.Usage}"); ImGui.TableNextColumn(); + ImGui.Text($"{texture.Resource.Format}"); + ImGui.TableNextColumn(); ImGui.Text($"{texture.HandlePath}"); } } diff --git a/Meddle/Meddle.Plugin/Utility/DXHelper.cs b/Meddle/Meddle.Plugin/Utility/DXHelper.cs index dada94f..2bf5e64 100644 --- a/Meddle/Meddle.Plugin/Utility/DXHelper.cs +++ b/Meddle/Meddle.Plugin/Utility/DXHelper.cs @@ -1,9 +1,10 @@ using System.Runtime.InteropServices; -using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using OtterTex; using SharpGen.Runtime; using Vortice.Direct3D11; using Format = Vortice.DXGI.Format; +using MapFlags = Vortice.Direct3D11.MapFlags; namespace Meddle.Plugin.Utility; @@ -18,37 +19,55 @@ public static TextureHelper.TextureResource ExportTextureResource(Texture* kerne tex.AddRef(); var ret = GetResourceData(tex, - r => - { - var desc = r.Description1 with - { - Usage = ResourceUsage.Staging, - BindFlags = 0, - CPUAccessFlags = CpuAccessFlags.Read, - MiscFlags = 0, - }; - - return r.Device.As().CreateTexture2D1(desc); - }, - (r, map) => - { - var desc = r.Description1; - - if (desc.Format is Format.BC1_UNorm or Format.BC2_UNorm or Format.BC3_UNorm or Format.BC4_UNorm or Format.BC5_UNorm) - { - if (map.RowPitch * Math.Max(1, (desc.Height + 3) / 4) != map.DepthPitch) - throw new InvalidDataException($"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; Block Height = {Math.Max(1, (desc.Height + 3) / 4)}; DepthPitch = {map.DepthPitch}"); - } - else - { - if (map.RowPitch * desc.Height != map.DepthPitch) - throw new InvalidDataException($"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; DepthPitch = {map.DepthPitch}"); - } - - var buf = new byte[map.DepthPitch]; - Marshal.Copy(map.DataPointer, buf, 0, buf.Length); - return new TextureHelper.TextureResource(desc.Format, desc.Width, desc.Height, map.RowPitch, buf); - }); + r => + { + var desc = r.Description1 with + { + Usage = ResourceUsage.Staging, + BindFlags = 0, + CPUAccessFlags = CpuAccessFlags.Read, + MiscFlags = 0, + }; + + return r.Device.As().CreateTexture2D1(desc); + }, + (r, map) => + { + var desc = r.Description1; + if (desc.Format is + Format.BC1_UNorm or + Format.BC2_UNorm or + Format.BC3_UNorm or + Format.BC4_UNorm or + Format.BC5_UNorm or + Format.BC7_UNorm) + { + var blockHeight = Math.Max(1, (desc.Height + 3) / 4); + if (map.RowPitch * blockHeight != map.DepthPitch) + throw new InvalidDataException($"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; Block Height = {blockHeight}; DepthPitch = {map.DepthPitch}"); + } + else + { + if (map.RowPitch * desc.Height != map.DepthPitch) + throw new InvalidDataException($"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; DepthPitch = {map.DepthPitch}"); + } + + var buf = new byte[map.DepthPitch]; + Marshal.Copy(map.DataPointer, buf, 0, buf.Length); + var resource = new TextureHelper.TextureResource( + // Vortice and OtterTex use different enums but the values *should* be the same so just cast + (DXGIFormat)desc.Format, + desc.Width, + desc.Height, + map.RowPitch, + desc.MipLevels, + desc.ArraySize, + (TexDimension)r.Dimension, + (D3DResourceMiscFlags)desc.MiscFlags, + buf); + + return resource; + }); return ret; } diff --git a/Meddle/Meddle.Plugin/Utility/TextureHelper.cs b/Meddle/Meddle.Plugin/Utility/TextureHelper.cs index 5b103ff..c4be003 100644 --- a/Meddle/Meddle.Plugin/Utility/TextureHelper.cs +++ b/Meddle/Meddle.Plugin/Utility/TextureHelper.cs @@ -1,32 +1,23 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using Dalamud.Logging; -using Lumina.Data.Parsing.Tex; using Meddle.Plugin.Models; using SkiaSharp; -using Vortice.DXGI; -using LuminaFormat = Lumina.Data.Files.TexFile.TextureFormat; +using OtterTex; namespace Meddle.Plugin.Utility; public static class TextureHelper { - public readonly struct TextureResource + public readonly struct TextureResource(DXGIFormat format, int width, int height, int stride, int mipLevels, int arraySize, TexDimension dimension, D3DResourceMiscFlags miscFlags, byte[] data) { - public TextureResource(Format format, int width, int height, int stride, byte[] data) - { - this.Format = format; - this.Width = width; - this.Height = height; - this.Stride = stride; - this.Data = data; - } - public Format Format { get; init; } - public int Width { get; init; } - public int Height { get; init; } - public int Stride { get; init; } - public byte[] Data { get; init; } - + public DXGIFormat Format { get; init; } = format; + public int Width { get; init; } = width; + public int Height { get; init; } = height; + public int Stride { get; init; } = stride; + public int MipLevels { get; init; } = mipLevels; + public int ArraySize { get; init; } = arraySize; + public TexDimension Dimension { get; init; } = dimension; + public D3DResourceMiscFlags MiscFlags { get; init; } = miscFlags; + public byte[] Data { get; init; } = data; + public SKTexture ToTexture((int width, int height)? resize = null) { var bitmap = ToBitmap(this); @@ -38,8 +29,85 @@ public SKTexture ToTexture((int width, int height)? resize = null) return new SKTexture(bitmap); } } + + private static readonly object LockObj = new(); + public static unsafe SKBitmap ToBitmap(TextureResource resource) + { + lock (LockObj) + { + var meta = FromResource(resource); + var image = ScratchImage.Initialize(meta); + + // copy data - ensure destination not too short + fixed (byte* data = image.Pixels) + { + var span = new Span(data, image.Pixels.Length); + if (resource.Data.Length > span.Length) + { + // As far as I can tell this only happens when placeholder textures are used anyways + Service.Log.Warning($"Data too large for scratch image. " + + $"{resource.Data.Length} > {span.Length} " + + $"{resource.Width}x{resource.Height} {resource.Format}"); + } + else + { + resource.Data.AsSpan().CopyTo(span); + } + } + + image.GetRGBA(out var rgba); + + var bitmap = new SKBitmap(rgba.Meta.Width, rgba.Meta.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul); + bitmap.Erase(new SKColor(0)); + fixed (byte* data = rgba.Pixels) + { + bitmap.InstallPixels(bitmap.Info, (nint)data, rgba.Meta.Width * 4); + } + + // I have trust issues + var copy = new SKBitmap(bitmap.Info); + bitmap.CopyTo(copy, bitmap.Info.ColorType); + + return copy; + } + } + + private static TexMeta FromResource(TextureResource resource) + { + var meta = new TexMeta + { + Height = resource.Height, + Width = resource.Width, + Depth = 1, // 3D textures would have other values, but we're only handling kernelTexture->D3D11Texture2D + MipLevels = resource.MipLevels, + ArraySize = resource.ArraySize, + Format = resource.Format, + Dimension = resource.Dimension, + MiscFlags = resource.MiscFlags.HasFlag(D3DResourceMiscFlags.TextureCube) ? D3DResourceMiscFlags.TextureCube : 0, + MiscFlags2 = 0, + }; + + return meta; + } - public static TextureResource FromTexFile(Lumina.Data.Files.TexFile file) + public static byte[] AdjustStride(int oldStride, int newStride, int height, byte[] data) + { + if (data.Length != oldStride * height) + throw new ArgumentException("Data length must match stride * height.", nameof(data)); + + if (oldStride == newStride) + return data; + if (oldStride < newStride) + throw new ArgumentException("New stride must be smaller than old stride.", nameof(newStride)); + + var newData = new byte[newStride * height]; + for (var y = 0; y < height; ++y) + Buffer.BlockCopy(data, y * oldStride, newData, y * newStride, newStride); + return newData; + } + + /* + public static TextureResource FromTexFile(TexFile file) { var format = file.Header.Format switch { @@ -85,23 +153,7 @@ public static TextureResource FromTexFile(Lumina.Data.Files.TexFile file) return new TextureResource(format, file.Header.Width, file.Header.Height, stride, file.Data); } - - public static byte[] AdjustStride(int oldStride, int newStride, int height, byte[] data) - { - if (data.Length != oldStride * height) - throw new ArgumentException("Data length must match stride * height.", nameof(data)); - - if (oldStride == newStride) - return data; - if (oldStride < newStride) - throw new ArgumentException("New stride must be smaller than old stride.", nameof(newStride)); - - var newData = new byte[newStride * height]; - for (var y = 0; y < height; ++y) - Buffer.BlockCopy(data, y * oldStride, newData, y * newStride, newStride); - return newData; - } - + public static byte[] ToBGRA8(TextureResource resource) { var ret = new byte[resource.Width * resource.Height * 4]; @@ -173,7 +225,7 @@ public static unsafe SKBitmap ToBitmap(TextureResource resource) bitmap.InstallPixels(format.WithSize(resource.Width, resource.Height), (nint)data, resource.Width * 4); } s.Stop(); - //PluginLog.Log($"ToBitmap ({(direct ? "SkiaSharp" : "Software")}) took {s.Elapsed.TotalMilliseconds}ms for {resource.Width}x{resource.Height} {resource.Format}"); + Service.Log.Debug($"ToBitmap ({(direct ? "SkiaSharp" : "Software")}) took {s.Elapsed.TotalMilliseconds}ms for {resource.Width}x{resource.Height} {resource.Format}"); // make copy so we can dispose the original var copy = new SKBitmap(bitmap.Info); @@ -354,4 +406,5 @@ private static void FromBC(TextureResource resource, Span output) }; Squish.DecompressImage(resource.Data, resource.Width, resource.Height, flags).CopyTo(MemoryMarshal.Cast(output)); } + */ }