diff --git a/Penumbra.GameData b/Penumbra.GameData index 33de79bc..d5f92966 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c +Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ec5784f8..df44a51a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -110,6 +110,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; public bool EditRawTileTransforms { get; set; } = false; + public bool HdrRenderTargets { get; set; } = true; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 2aeeb14b..b95e5789 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -86,6 +86,7 @@ public class HookOverrides public bool ModelRendererOnRenderMaterial; public bool ModelRendererUnkFunc; public bool PrepareColorTable; + public bool RenderTargetManagerInitialize; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs new file mode 100644 index 00000000..b7ae771b --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -0,0 +1,137 @@ +using System.Collections.Immutable; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public unsafe class RenderTargetHdrEnabler : IService, IDisposable +{ + /// This array must be sorted by CreationOrder ascending. + private static readonly ImmutableArray ForcedTextureConfigs = + [ + new ForcedTextureConfig(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), + new ForcedTextureConfig(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), + ]; + + private static readonly IComparer ForcedTextureConfigComparer + = Comparer.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder)); + + private readonly Configuration _config; + + private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); + + private readonly ThreadLocal?> _textures = new(() => null); + + public TextureReportRecord[]? TextureReport { get; private set; } + + [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] + private readonly Hook _renderTargetManagerInitialize = null!; + + [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] + private readonly Hook _createTexture2D = null!; + + public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) + { + _config = config; + interop.InitializeFromAttributes(this); + if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) + _renderTargetManagerInitialize.Enable(); + } + + ~RenderTargetHdrEnabler() + => Dispose(false); + + public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) + { + var i = ForcedTextureConfigs.BinarySearch(new ForcedTextureConfig(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); + return i >= 0 ? ForcedTextureConfigs[i] : null; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool _) + { + _createTexture2D.Dispose(); + _renderTargetManagerInitialize.Dispose(); + } + + private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) + { + _createTexture2D.Enable(); + _textureIndices.Value = new TextureIndices(0, 0); + _textures.Value = _config.DebugMode ? [] : null; + try + { + return _renderTargetManagerInitialize.Original(@this); + } + finally + { + if (_textures.Value != null) + { + TextureReport = CreateTextureReport(@this, _textures.Value); + _textures.Value = null; + } + + _textureIndices.Value = new TextureIndices(-1, -1); + _createTexture2D.Disable(); + } + } + + private Texture* CreateTexture2DDetour( + Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + { + var originalTextureFormat = textureFormat; + var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new TextureIndices(-1, -1); + if (indices.ConfigIndex >= 0 + && indices.ConfigIndex < ForcedTextureConfigs.Length + && ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) + { + var config = ForcedTextureConfigs[indices.ConfigIndex++]; + textureFormat = (uint)config.ForcedTextureFormat; + } + + if (indices.CreationOrder >= 0) + { + ++indices.CreationOrder; + _textureIndices.Value = indices; + } + + var texture = _createTexture2D.Original(@this, size, mipLevel, textureFormat, flags, unk); + if (_textures.IsValueCreated) + _textures.Value?.Add((nint)texture, (indices.CreationOrder - 1, originalTextureFormat)); + return texture; + } + + private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, + Dictionary textures) + { + var rtmTextures = new Span(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint)); + var report = new List(); + for (var i = 0; i < rtmTextures.Length; ++i) + { + if (textures.TryGetValue(rtmTextures[i], out var texture)) + report.Add(new TextureReportRecord(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); + } + + return report.ToArray(); + } + + private delegate nint RenderTargetManagerInitializeFunc(RenderTargetManager* @this); + + private delegate Texture* CreateTexture2DFunc(Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk); + + private record struct TextureIndices(int CreationOrder, int ConfigIndex); + + public readonly record struct ForcedTextureConfig(int CreationOrder, TextureFormat ForcedTextureFormat, string Comment); + + public readonly record struct TextureReportRecord(nint Offset, int CreationOrder, TextureFormat OriginalTextureFormat); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 40958eb4..f70ea06e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -7,6 +7,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.Services; @@ -63,8 +64,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - private readonly ModelRenderer _modelRenderer; private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; @@ -110,31 +109,31 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _modelRenderer = modelRenderer; - _communicator = communicator; - _humanSetupScalingHook = humanSetupScalingHook; + var utility1 = utility; + var modelRenderer1 = modelRenderer; + _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultSkinShpkResource); _characterStockingsState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterStockingsShpkResource); _characterLegacyState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); - _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); - _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, - () => _modelRenderer.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTransparencyShaderPackage, - () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTattooShaderPackage, - () => _modelRenderer.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new ModdedShaderPackageState(() => _modelRenderer.CharacterOcclusionShaderPackage, - () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterLegacyShpkResource); + _irisState = new ModdedShaderPackageState(() => modelRenderer1.IrisShaderPackage, () => modelRenderer1.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => modelRenderer1.CharacterGlassShaderPackage, + () => modelRenderer1.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTransparencyShaderPackage, + () => modelRenderer1.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTattooShaderPackage, + () => modelRenderer1.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer1.CharacterOcclusionShaderPackage, + () => modelRenderer1.DefaultCharacterOcclusionShaderPackage); _hairMaskState = - new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + new ModdedShaderPackageState(() => modelRenderer1.HairMaskShaderPackage, () => modelRenderer1.DefaultHairMaskShaderPackage); _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], @@ -462,8 +461,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return mtrlResource; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int GetDataSetExpectedSize(uint dataFlags) + => (dataFlags & 4) != 0 + ? ColorTable.Size + ((dataFlags & 8) != 0 ? ColorDyeTable.Size : 0) + : 0; + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { + if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) + Penumbra.Log.Warning( + $"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForColorTable() == 0) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 4790da18..968bb750 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -10,7 +10,7 @@ "Tags": [ "modding" ], "DalamudApiLevel": 11, "LoadPriority": 69420, - "LoadState": 2, + "LoadRequiredState": 2, "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8b2bcd77..77eeb3d7 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -104,6 +104,7 @@ public class DebugTab : Window, ITab, IUiService private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; + private readonly RenderTargetDrawer _renderTargetDrawer; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -114,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -154,6 +155,7 @@ public class DebugTab : Window, ITab, IUiService _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; _objectIdentification = objectIdentification; + _renderTargetDrawer = renderTargetDrawer; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -189,6 +191,7 @@ public class DebugTab : Window, ITab, IUiService DrawData(); DrawCrcCache(); DrawResourceProblems(); + _renderTargetDrawer.Draw(); _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs new file mode 100644 index 00000000..09c8b06c --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -0,0 +1,59 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using ImGuiNET; +using OtterGui; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Hooks.PostProcessing; + +namespace Penumbra.UI.Tabs.Debug; + +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler) : IUiService +{ + /// Draw information about render targets. + public unsafe void Draw() + { + if (!ImUtf8.CollapsingHeader("Render Targets"u8)) + return; + + var report = renderTargetHdrEnabler.TextureReport; + if (report == null) + { + ImUtf8.Text("The RenderTargetManager report has not been gathered."u8); + ImUtf8.Text("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."u8); + return; + } + + using var table = ImUtf8.Table("##RenderTargetTable"u8, 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var record in report) + { + ImUtf8.DrawTableColumn($"0x{record.Offset:X}"); + ImUtf8.DrawTableColumn($"{record.CreationOrder}"); + ImUtf8.DrawTableColumn($"{record.OriginalTextureFormat}"); + ImGui.TableNextColumn(); + var texture = *(Texture**)((nint)RenderTargetManager.Instance() + + record.Offset); + if (texture != null) + { + using var color = Dalamud.Interface.Utility.Raii.ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), + texture->TextureFormat != record.OriginalTextureFormat); + ImUtf8.Text($"{texture->TextureFormat}"); + } + + ImGui.TableNextColumn(); + var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); + if (forcedConfig.HasValue) + ImUtf8.Text(forcedConfig.Value.Comment); + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 46e214cf..c7f66859 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -10,6 +10,7 @@ using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; @@ -773,6 +774,7 @@ public class SettingsTab : ITab, IUiService DrawCrashHandler(); DrawMinimumDimensionConfig(); + DrawHdrRenderTargets(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); @@ -902,6 +904,33 @@ public class SettingsTab : ITab, IUiService _config.Save(); } + private void DrawHdrRenderTargets() + { + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("M"u8).X * 5.0f + ImGui.GetFrameHeight()); + using (var combo = ImUtf8.Combo("##hdrRenderTarget"u8, _config.HdrRenderTargets ? "HDR"u8 : "SDR"u8)) + { + if (combo) + { + if (ImUtf8.Selectable("HDR"u8, _config.HdrRenderTargets) && !_config.HdrRenderTargets) + { + _config.HdrRenderTargets = true; + _config.Save(); + } + + if (ImUtf8.Selectable("SDR"u8, !_config.HdrRenderTargets) && _config.HdrRenderTargets) + { + _config.HdrRenderTargets = false; + _config.Save(); + } + } + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Diffuse Dynamic Range"u8, + "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\n"u8 + + "Changing this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."u8); + } + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() {