Merge branch 'rt-hdr'

This commit is contained in:
Ottermandias 2025-01-11 14:21:12 +01:00
commit aebd22ed64
9 changed files with 264 additions and 25 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,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
{
/// <remarks> This array must be sorted by CreationOrder ascending. </remarks>
private static readonly ImmutableArray<ForcedTextureConfig> ForcedTextureConfigs =
[
new ForcedTextureConfig(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"),
new ForcedTextureConfig(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent 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 TextureIndices(-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 readonly Hook<RenderTargetManagerInitializeFunc> _renderTargetManagerInitialize = null!;
[Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))]
private readonly 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 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<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 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);
}

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;
@ -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<CharacterBaseOnRenderMaterialDelegate>("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);

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

@ -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();

View file

@ -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
{
/// <summary> Draw information about render targets. </summary>
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);
}
}
}

View file

@ -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);
}
/// <summary> Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. </summary>
private void DrawEnableHttpApiBox()
{