Add RenderTargetHdrEnabler

This commit is contained in:
Exter-N 2025-01-08 20:02:14 +01:00
parent 349241d0ab
commit f07780cf7b
8 changed files with 219 additions and 3 deletions

@ -1 +1 @@
Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c
Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37

View file

@ -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<ColorId, uint> Colors { get; set; }
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);

View file

@ -86,6 +86,7 @@ public class HookOverrides
public bool ModelRendererOnRenderMaterial;
public bool ModelRendererUnkFunc;
public bool PrepareColorTable;
public bool RenderTargetManagerInitialize;
}
public struct ResourceLoadingHooks

View file

@ -0,0 +1,136 @@
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
{
/// <remarks> This array must be sorted by CreationOrder ascending. </remarks>
private static readonly ImmutableArray<ForcedTextureConfig> ForcedTextureConfigs =
[
new(9, TextureFormat.R16G16B16A16_FLOAT, "Main Diffuse GBuffer"),
new(10, TextureFormat.R16G16B16A16_FLOAT, "Hair Diffuse GBuffer"),
];
private static readonly IComparer<ForcedTextureConfig> ForcedTextureConfigComparer
= Comparer<ForcedTextureConfig>.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder));
private readonly Configuration _config;
private readonly ThreadLocal<TextureIndices> _textureIndices = new(() => new(-1, -1));
private readonly ThreadLocal<Dictionary<nint, (int TextureIndex, uint TextureFormat)>?> _textures = new(() => null);
public TextureReportRecord[]? TextureReport { get; private set; }
[Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))]
private Hook<RenderTargetManagerInitializeFunc> _renderTargetManagerInitialize = null!;
[Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))]
private Hook<CreateTexture2DFunc> _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(creationOrder, 0, string.Empty), ForcedTextureConfigComparer);
return i >= 0 ? ForcedTextureConfigs[i] : null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool _)
{
_renderTargetManagerInitialize.Disable();
if (_createTexture2D.IsEnabled)
_createTexture2D.Disable();
_createTexture2D.Dispose();
_renderTargetManagerInitialize.Dispose();
}
private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this)
{
_createTexture2D.Enable();
_textureIndices.Value = new(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(-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(-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<nint, (int TextureIndex, uint TextureFormat)> textures)
{
var rtmTextures = new Span<nint>(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint));
var report = new List<TextureReportRecord>();
for (var i = 0; i < rtmTextures.Length; ++i)
{
if (textures.TryGetValue(rtmTextures[i], out var texture))
report.Add(new(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);
}

View file

@ -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;
@ -462,8 +463,16 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
return mtrlResource;
}
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);

View file

@ -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"
}

View file

@ -42,6 +42,7 @@ using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
using CSGraphics = FFXIVClientStructs.FFXIV.Client.Graphics;
namespace Penumbra.UI.Tabs.Debug;
@ -104,6 +105,7 @@ public class DebugTab : Window, ITab, IUiService
private readonly RsfService _rsfService;
private readonly SchedulerResourceManagementService _schedulerService;
private readonly ObjectIdentification _objectIdentification;
private readonly RenderTargetHdrEnabler _renderTargetHdrEnabler;
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects,
IClientState clientState, IDataManager dataManager,
@ -114,7 +116,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, RenderTargetHdrEnabler renderTargetHdrEnabler)
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
{
IsOpen = true;
@ -154,6 +156,7 @@ public class DebugTab : Window, ITab, IUiService
_globalVariablesDrawer = globalVariablesDrawer;
_schedulerService = schedulerService;
_objectIdentification = objectIdentification;
_renderTargetHdrEnabler = renderTargetHdrEnabler;
_objects = objects;
_clientState = clientState;
_dataManager = dataManager;
@ -189,6 +192,7 @@ public class DebugTab : Window, ITab, IUiService
DrawData();
DrawCrcCache();
DrawResourceProblems();
DrawRenderTargets();
_hookOverrides.Draw();
DrawPlayerModelInfo();
_globalVariablesDrawer.Draw();
@ -1135,6 +1139,54 @@ public class DebugTab : Window, ITab, IUiService
}
/// <summary> Draw information about render targets. </summary>
private unsafe void DrawRenderTargets()
{
if (!ImGui.CollapsingHeader("Render Targets"))
return;
var report = _renderTargetHdrEnabler.TextureReport;
if (report == null)
{
ImGui.TextUnformatted("The RenderTargetManager report has not been gathered.");
ImGui.TextUnformatted("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section.");
return;
}
using var table = Table("##RenderTargetTable", 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)
{
ImGui.TableNextColumn();
ImUtf8.Text($"0x{record.Offset:X}");
ImGui.TableNextColumn();
ImUtf8.Text($"{record.CreationOrder}");
ImGui.TableNextColumn();
ImUtf8.Text($"{record.OriginalTextureFormat}");
ImGui.TableNextColumn();
var texture = *(CSGraphics.Kernel.Texture**)((nint)CSGraphics.Render.RenderTargetManager.Instance() + record.Offset);
if (texture != null)
{
using var color = 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)
ImGui.TextUnformatted(forcedConfig.Value.Comment);
}
}
/// <summary> Draw information about IPC options and availability. </summary>
private void DrawDebugTabIpc()
{

View file

@ -773,6 +773,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 +903,22 @@ public class SettingsTab : ITab, IUiService
_config.Save();
}
private void DrawHdrRenderTargets()
{
var item = _config.HdrRenderTargets ? 1 : 0;
ImGui.SetNextItemWidth(ImGui.CalcTextSize("M").X * 5.0f + ImGui.GetFrameHeight());
var edited = ImGui.Combo("##hdrRenderTarget", ref item, "SDR\0HDR\0");
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker("Diffuse Dynamic Range",
"Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\nChanging this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled.");
if (!edited)
return;
_config.HdrRenderTargets = item != 0;
_config.Save();
}
/// <summary> Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. </summary>
private void DrawEnableHttpApiBox()
{