From ff12d438c6f75cec36253a4cc1d9f252a0092929 Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Sun, 29 Jun 2025 16:52:20 +0200 Subject: [PATCH] core (0.4): Implement separate display vs game scaling (thanks, NotNite!) --- .editorconfig | 5 + CustomResolution2782.sln | 1 + CustomResolution2782/Cmds/GameResCmd.cs | 79 ++++ CustomResolution2782/Cmds/MainCmd.cs | 32 +- CustomResolution2782/Configuration.cs | 152 ++----- CustomResolution2782/ConfigurationEnums.cs | 147 +++++++ CustomResolution2782/ConfigurationV1.cs | 66 +++ .../CustomResolution2782.csproj | 2 +- CustomResolution2782/DisplaySizeState.cs | 407 ++++++++++++++++++ CustomResolution2782/GameSizeState.cs | 149 +++++++ CustomResolution2782/Hooks/CursorPosHooks.cs | 4 +- CustomResolution2782/Hooks/WindowRectHooks.cs | 8 +- CustomResolution2782/Hooks/WndProcHook.cs | 16 +- CustomResolution2782/Plugin.cs | 405 ++--------------- CustomResolution2782/Service.cs | 3 + CustomResolution2782/Windows/ConfigWindow.cs | 247 +++++++---- 16 files changed, 1126 insertions(+), 597 deletions(-) create mode 100644 .editorconfig create mode 100644 CustomResolution2782/Cmds/GameResCmd.cs create mode 100644 CustomResolution2782/ConfigurationEnums.cs create mode 100644 CustomResolution2782/ConfigurationV1.cs create mode 100644 CustomResolution2782/DisplaySizeState.cs create mode 100644 CustomResolution2782/GameSizeState.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1edd50 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*.cs] + +# CS9084: Struct member returns 'this' or other instance members by reference +# While this would be a huge no-no in fully managed projects, we use structs to poke game memory. +dotnet_diagnostic.CS9084.severity = silent diff --git a/CustomResolution2782.sln b/CustomResolution2782.sln index 07c286f..7a7793c 100644 --- a/CustomResolution2782.sln +++ b/CustomResolution2782.sln @@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResolution2782", "Cus EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ECEDCE5-D60F-4A8A-AB33-4131F5C7371C}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props EndProjectSection EndProject diff --git a/CustomResolution2782/Cmds/GameResCmd.cs b/CustomResolution2782/Cmds/GameResCmd.cs new file mode 100644 index 0000000..08ebae7 --- /dev/null +++ b/CustomResolution2782/Cmds/GameResCmd.cs @@ -0,0 +1,79 @@ +using System.Globalization; +using System; + +namespace CustomResolution.Cmds; + +public sealed class GameResCmd : Cmd +{ + public override string Name => "gres"; + + public override string HelpMessage => $"Open / adjust the CustomResolution game scale settings.\n" + + $"\tExamples:\n" + + $"\tTo open the settings:\n\t\t{FullName}\n" + + $"\tTo enable / disable it:\n\t\t{FullName} on\n\t\t{FullName} off\n\t\t{FullName} toggle\n" + + $"\tTo set the game scale:\n\t\t{FullName} 1.5\n" + + $"\tTo set the game resolution:\n\t\t{FullName} 1920 1080"; + + public override void Run(string arguments) + { + if (string.IsNullOrEmpty(arguments)) + { + Service.PluginUI.SettingsVisible = !Service.PluginUI.SettingsVisible; + + return; + } + + int indexOfSplit = arguments.IndexOf(' '); + + if (indexOfSplit != -1) + { + if (!uint.TryParse(arguments.AsSpan(0, indexOfSplit), CultureInfo.InvariantCulture, out var width) || + !uint.TryParse(arguments.AsSpan(indexOfSplit + 1), CultureInfo.InvariantCulture, out var height)) + { + Service.PrintChat("Invalid parameters."); + return; + } + + Service.Config._.Game.IsScale = false; + Service.Config._.Game.Width = width; + Service.Config._.Game.Height = height; + Service.Config.Save(); + + Service.PrintChat("Updated custom game resolution."); + return; + } + + switch (arguments.ToLowerInvariant()) + { + case "on": + Service.Config._.Game.IsEnabled = true; + Service.Config.Save(); + Service.PrintChat("Enabled custom game resolution."); + return; + + case "off": + Service.Config._.Game.IsEnabled = false; + Service.Config.Save(); + Service.PrintChat("Disabled custom game resolution."); + return; + + case "toggle": + Service.Config._.Game.IsEnabled = !Service.Config._.Game.IsEnabled; + Service.Config.Save(); + Service.PrintChat($"{(Service.Config._.Game.IsEnabled ? "Enabled" : "Disabled")} custom game resolution."); + return; + } + + if (!float.TryParse(arguments, CultureInfo.InvariantCulture, out float value)) + { + Service.PrintChat("Invalid parameters."); + return; + } + + Service.Config._.Game.IsScale = true; + Service.Config._.Game.Scale = value; + Service.Config.Save(); + + Service.PrintChat("Updated custom game resolution scale."); + } +} diff --git a/CustomResolution2782/Cmds/MainCmd.cs b/CustomResolution2782/Cmds/MainCmd.cs index 68b7ce2..5776626 100644 --- a/CustomResolution2782/Cmds/MainCmd.cs +++ b/CustomResolution2782/Cmds/MainCmd.cs @@ -7,12 +7,12 @@ public sealed class MainCmd : Cmd { public override string Name => "cres"; - public override string HelpMessage => $"Open / adjust the CustomResolution settings.\n" + + public override string HelpMessage => $"Open / adjust the CustomResolution display scale settings.\n" + $"\tExamples:\n" + $"\tTo open the settings:\n\t\t{FullName}\n" + $"\tTo enable / disable it:\n\t\t{FullName} on\n\t\t{FullName} off\n\t\t{FullName} toggle\n" + - $"\tTo set the scale:\n\t\t{FullName} 1.5\n" + - $"\tTo set the resolution:\n\t\t{FullName} 1920 1080"; + $"\tTo set the display scale:\n\t\t{FullName} 1.5\n" + + $"\tTo set the display resolution:\n\t\t{FullName} 1920 1080"; public override void Run(string arguments) { @@ -34,33 +34,33 @@ public sealed class MainCmd : Cmd return; } - Service.Config.IsScale = false; - Service.Config.Width = width; - Service.Config.Height = height; + Service.Config._.Display.IsScale = false; + Service.Config._.Display.Width = width; + Service.Config._.Display.Height = height; Service.Config.Save(); - Service.PrintChat("Updated custom resolution."); + Service.PrintChat("Updated custom display resolution."); return; } switch (arguments.ToLowerInvariant()) { case "on": - Service.Config.IsEnabled = true; + Service.Config._.Display.IsEnabled = true; Service.Config.Save(); - Service.PrintChat("Enabled custom resolution."); + Service.PrintChat("Enabled custom display resolution."); return; case "off": - Service.Config.IsEnabled = false; + Service.Config._.Display.IsEnabled = false; Service.Config.Save(); - Service.PrintChat("Disabled custom resolution."); + Service.PrintChat("Disabled custom display resolution."); return; case "toggle": - Service.Config.IsEnabled = !Service.Config.IsEnabled; + Service.Config._.Display.IsEnabled = !Service.Config._.Display.IsEnabled; Service.Config.Save(); - Service.PrintChat($"{(Service.Config.IsEnabled ? "Enabled" : "Disabled")} custom resolution."); + Service.PrintChat($"{(Service.Config._.Display.IsEnabled ? "Enabled" : "Disabled")} custom display resolution."); return; case "debugon": @@ -95,10 +95,10 @@ public sealed class MainCmd : Cmd return; } - Service.Config.IsScale = true; - Service.Config.Scale = value; + Service.Config._.Display.IsScale = true; + Service.Config._.Display.Scale = value; Service.Config.Save(); - Service.PrintChat("Updated custom resolution scale."); + Service.PrintChat("Updated custom display resolution scale."); } } diff --git a/CustomResolution2782/Configuration.cs b/CustomResolution2782/Configuration.cs index 9b06482..3935425 100644 --- a/CustomResolution2782/Configuration.cs +++ b/CustomResolution2782/Configuration.cs @@ -1,151 +1,77 @@ using Dalamud.Configuration; using Dalamud.Game.ClientState.Keys; using Dalamud.Plugin; +using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.Reflection; namespace CustomResolution; [Serializable] public class Configuration : IPluginConfiguration { - public int Version { get; set; } = 0; + public ConfigurationV1 V1 = new(); + public int Version { get; set; } = 1; + [JsonIgnore] + public ref ConfigurationV1 _ => ref V1; + + [Obsolete] public bool IsEnabled = true; + [Obsolete] public bool IsScale = true; + [Obsolete] public float Scale = 1f; + [Obsolete] public uint Width = 1024; + [Obsolete] public uint Height = 1024; - + [Obsolete] public VirtualKey HotkeyKey = VirtualKey.NO_KEY; + [Obsolete] public ModifierKey HotkeyModifier = ModifierKey.NONE; - + [Obsolete] public DXVKDWMHackMode DXVKDWMHackMode = DXVKDWMHackMode.Off; - + [Obsolete] public MinSizeMode MinSizeMode = MinSizeMode.Unchanged; [NonSerialized] - private IDalamudPluginInterface? pluginInterface; + private IDalamudPluginInterface pluginInterface = null!; internal void Initialize(IDalamudPluginInterface pluginInterface) { this.pluginInterface = pluginInterface; + Upgrade(); } public void Save() { - pluginInterface!.SavePluginConfig(this); + _.Display.Clamp(); + _.Game.Clamp(); + pluginInterface.SavePluginConfig(this); Service.PluginUI.UpdateFromConfig(); } -} -// Must explicitly map to integer values, blame Newtonsoft.JSON -public enum DXVKDWMHackMode -{ - Off = 0, - UnsetPopup = 1, - SetClientEdgeResize = 3, - SetClientEdgeBorder = 2, -} - -public static class DXVKDWMHackModeExt -{ - public static string ToHumanNameString(this DXVKDWMHackMode mode) => mode switch + private void Upgrade() { - DXVKDWMHackMode.Off => "Off", - DXVKDWMHackMode.UnsetPopup => "Best: -WS_POPUP", - DXVKDWMHackMode.SetClientEdgeResize => "+WS_EX_CLIENTEDGE (resize)", - DXVKDWMHackMode.SetClientEdgeBorder => "+WS_EX_CLIENTEDGE (border)", - _ => mode.ToString(), - }; - - public static string? ToHumanInfoString(this DXVKDWMHackMode mode) => mode switch - { - DXVKDWMHackMode.UnsetPopup => "Least intrusive option, try this first.\nWorks best with NVIDIA GPUs.", - DXVKDWMHackMode.SetClientEdgeResize => "Extends the game window 1 pixel to the bottom.\nDon't use if it makes text look blurry!", - DXVKDWMHackMode.SetClientEdgeBorder => "Adds a 1 pixel border around the game.", - _ => null - }; - - public static bool IsUnsetPopup(this DXVKDWMHackMode mode) => mode == DXVKDWMHackMode.UnsetPopup; - - public static bool IsSetClientEdge(this DXVKDWMHackMode mode) => mode switch - { - DXVKDWMHackMode.SetClientEdgeResize => true, - DXVKDWMHackMode.SetClientEdgeBorder => true, - _ => false - }; -} - -public enum ModifierKey -{ - NONE = 0, - CTRL = 1, - ALT = 2, - SHIFT = 4 -} - -public static class VirtualKeyExt -{ - private static readonly Dictionary _strings = new(); - - static VirtualKeyExt() - { - foreach (var field in typeof(VirtualKey).GetFields(BindingFlags.Public | BindingFlags.Static)) +#pragma warning disable CS0612 // Type or member is obsolete, but we want to migrate from it. +#pragma warning disable CS0618 // Type or member is obsolete, but we want to migrate from it. + if (Version == 0) { - var key = (VirtualKey) field.GetValue(null)!; - _strings[key] = key.GetFancyName(); + Service.PluginLog.Info("Migrating config V0 to V1"); + V1 = new(); + // V1.Display.IsEnabled = IsEnabled; // Was enabled by default before, but let's disable. + V1.Display.IsScale = IsScale; + V1.Display.Scale = Scale; + V1.Display.Width = Width; + V1.Display.Height = Height; + V1.Display.HotkeyKey = HotkeyKey; + V1.Display.HotkeyModifier = HotkeyModifier; + V1.DXVKDWMHackMode = DXVKDWMHackMode; + V1.MinSizeMode = MinSizeMode; + V1.Display.Clamp(); + Version++; } +#pragma warning restore CS0612 +#pragma warning restore CS0618 } - - public static string ToHumanNameString(this VirtualKey mode) - { - return _strings[mode]; - } - - public static string? ToHumanInfoString(this VirtualKey mode) => mode switch - { - _ => null - }; -} - -public static class ModifierKeyExt -{ - public static string ToHumanNameString(this ModifierKey mode) => mode switch - { - ModifierKey.NONE => "None", - ModifierKey.CTRL => "Ctrl", - ModifierKey.ALT => "Alt", - ModifierKey.SHIFT => "Shift", - _ => mode.ToString(), - }; - - public static string? ToHumanInfoString(this ModifierKey mode) => mode switch - { - _ => null - }; -} - -public enum MinSizeMode -{ - Unchanged = 0, - Unlocked = 1 -} - -public static class MinSizeModeExt -{ - public static string ToHumanNameString(this MinSizeMode mode) => mode switch - { - MinSizeMode.Unchanged => "Unchanged (1024x768)", - MinSizeMode.Unlocked => "Unlocked", - _ => mode.ToString(), - }; - - public static string? ToHumanInfoString(this MinSizeMode mode) => mode switch - { - MinSizeMode.Unchanged => "The game normally doesn't allow resizing to smaller than 1024x768.", - MinSizeMode.Unlocked => "Allow resizing the game in windowed mode to smaller than 1024x768. Works best with scaling over 100%.", - _ => null - }; } diff --git a/CustomResolution2782/ConfigurationEnums.cs b/CustomResolution2782/ConfigurationEnums.cs new file mode 100644 index 0000000..e1d2b89 --- /dev/null +++ b/CustomResolution2782/ConfigurationEnums.cs @@ -0,0 +1,147 @@ +using Dalamud.Configuration; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Plugin; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace CustomResolution; + + +// Must explicitly map to integer values, blame Newtonsoft.JSON +public enum DXVKDWMHackMode +{ + Off = 0, + UnsetPopup = 1, + SetClientEdgeResize = 3, + SetClientEdgeBorder = 2, +} + +public static class DXVKDWMHackModeExt +{ + public static string ToHumanNameString(this DXVKDWMHackMode mode) => mode switch + { + DXVKDWMHackMode.Off => "Off", + DXVKDWMHackMode.UnsetPopup => "Best: -WS_POPUP", + DXVKDWMHackMode.SetClientEdgeResize => "+WS_EX_CLIENTEDGE (resize)", + DXVKDWMHackMode.SetClientEdgeBorder => "+WS_EX_CLIENTEDGE (border)", + _ => mode.ToString(), + }; + + public static string? ToHumanInfoString(this DXVKDWMHackMode mode) => mode switch + { + DXVKDWMHackMode.UnsetPopup => "Least intrusive option, try this first.\nWorks best with NVIDIA GPUs.", + DXVKDWMHackMode.SetClientEdgeResize => "Extends the game window 1 pixel to the bottom.\nDon't use if it makes text look blurry!", + DXVKDWMHackMode.SetClientEdgeBorder => "Adds a 1 pixel border around the game.", + _ => null + }; + + public static bool IsUnsetPopup(this DXVKDWMHackMode mode) => mode == DXVKDWMHackMode.UnsetPopup; + + public static bool IsSetClientEdge(this DXVKDWMHackMode mode) => mode switch + { + DXVKDWMHackMode.SetClientEdgeResize => true, + DXVKDWMHackMode.SetClientEdgeBorder => true, + _ => false + }; +} + +public enum ModifierKey +{ + NONE = 0, + CTRL = 1, + ALT = 2, + SHIFT = 4 +} + +public static class VirtualKeyExt +{ + private static readonly Dictionary _strings = new(); + + static VirtualKeyExt() + { + foreach (var field in typeof(VirtualKey).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var key = (VirtualKey) field.GetValue(null)!; + _strings[key] = key.GetFancyName(); + } + } + + public static string ToHumanNameString(this VirtualKey mode) + { + return _strings[mode]; + } + + public static string? ToHumanInfoString(this VirtualKey mode) => mode switch + { + _ => null + }; +} + +public static class ModifierKeyExt +{ + public static string ToHumanNameString(this ModifierKey mode) => mode switch + { + ModifierKey.NONE => "None", + ModifierKey.CTRL => "Ctrl", + ModifierKey.ALT => "Alt", + ModifierKey.SHIFT => "Shift", + _ => mode.ToString(), + }; + + public static string? ToHumanInfoString(this ModifierKey mode) => mode switch + { + _ => null + }; +} + +public enum MinSizeMode +{ + Unchanged = 0, + Unlocked = 1 +} + +public static class MinSizeModeExt +{ + public static string ToHumanNameString(this MinSizeMode mode) => mode switch + { + MinSizeMode.Unchanged => "Unchanged (1024x768)", + MinSizeMode.Unlocked => "Unlocked", + _ => mode.ToString(), + }; + + public static string? ToHumanInfoString(this MinSizeMode mode) => mode switch + { + MinSizeMode.Unchanged => "The game normally doesn't allow resizing to smaller than 1024x768.", + MinSizeMode.Unlocked => "Allow resizing the game in windowed mode to smaller than 1024x768. Works best with scaling over 100%.", + _ => null + }; +} + +public enum ResolutionScalingMode +{ + Fast = 0, + FSR = 1, + DLSS = 2 +} + +public static class ResolutionScalingModeExt +{ + public static string ToHumanNameString(this ResolutionScalingMode mode) => mode switch + { + ResolutionScalingMode.Fast => "Fast Pixelation", + ResolutionScalingMode.FSR => "AMD FSR", + ResolutionScalingMode.DLSS => "NVIDIA DLSS", + _ => mode.ToString(), + }; + + public static string? ToHumanInfoString(this ResolutionScalingMode mode) => mode switch + { + ResolutionScalingMode.Fast => "Lowest quality option which results in blurry pixelation.", + ResolutionScalingMode.FSR => "Upscaling which works on any GPU for low performance overhead.", + ResolutionScalingMode.DLSS => "DLSS in FFXIV is buggy, even without any plugins.", + _ => null + }; + + public static bool IsUnsupported(this ResolutionScalingMode mode) => mode == ResolutionScalingMode.DLSS; +} diff --git a/CustomResolution2782/ConfigurationV1.cs b/CustomResolution2782/ConfigurationV1.cs new file mode 100644 index 0000000..fbdbaa8 --- /dev/null +++ b/CustomResolution2782/ConfigurationV1.cs @@ -0,0 +1,66 @@ +using Dalamud.Configuration; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Plugin; +using System; + +namespace CustomResolution; + +[Serializable] +public struct ConfigurationV1 +{ + public SizeConfig Display = new(); + public SizeConfig Game = new(); + + public ResolutionScalingMode ResolutionScalingMode = ResolutionScalingMode.FSR; + public DXVKDWMHackMode DXVKDWMHackMode = DXVKDWMHackMode.Off; + public MinSizeMode MinSizeMode = MinSizeMode.Unchanged; + + public ConfigurationV1() + { + } + + [Serializable] + public struct SizeConfig + { + public bool IsEnabled = false; + public bool IsScale = true; + public float Scale = 1f; + [NonSerialized] + public unsafe fixed int WH[2]; + public unsafe uint Width + { + get => (uint) WH[0]; + set => WH[0] = (int) value; + } + public unsafe uint Height + { + get => (uint) WH[1]; + set => WH[1] = (int) value; + } + + public VirtualKey HotkeyKey = VirtualKey.NO_KEY; + public ModifierKey HotkeyModifier = ModifierKey.NONE; + + public SizeConfig() + { + Width = 1024; + Height = 1024; + } + + public void Clamp() + { + if (Width < 256) + { + Width = 256; + } + if (Height < 256) + { + Height = 256; + } + if (Scale < 0.001f) + { + Scale = 0.001f; + } + } + } +} diff --git a/CustomResolution2782/CustomResolution2782.csproj b/CustomResolution2782/CustomResolution2782.csproj index ad102bb..d003edb 100644 --- a/CustomResolution2782/CustomResolution2782.csproj +++ b/CustomResolution2782/CustomResolution2782.csproj @@ -3,7 +3,7 @@ 0x0ade - 0.3.2.0 + 0.4.0.0 diff --git a/CustomResolution2782/DisplaySizeState.cs b/CustomResolution2782/DisplaySizeState.cs new file mode 100644 index 0000000..ab2129a --- /dev/null +++ b/CustomResolution2782/DisplaySizeState.cs @@ -0,0 +1,407 @@ +using CustomResolution.Hooks; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Config; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using TerraFX.Interop.Windows; +using static TerraFX.Interop.Windows.Windows; + +namespace CustomResolution; + +public unsafe class DisplaySizeState : IDisposable +{ + private bool _wasEnabled = false; + private HWND _currentHwnd; + private bool _currentHwndMismatch = false; + private RECT _currentClientRect; + private DXVKDWMHackMode _currentDXVKDWMHackMode = DXVKDWMHackMode.Off; + private bool _currentlyFakeResize = false; + private bool _requestedResolutionChange = false; + + public HWND CurrentHWND => _currentHwnd; + + public uint CurrentWidth { get; private set; } + public uint CurrentHeight { get; private set; } + public uint CurrentWindowWidth { get; private set; } + public uint CurrentWindowHeight { get; private set; } + + public bool CurrentBorderlessFullscreen { get; private set; } + + public void Dispose() + { + Service.Framework.RunOnFrameworkThread(Update); + } + + public void ConvertCoordsWinToGame(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + var scaleX = CurrentWidth / (float) CurrentWindowWidth; + var scaleY = CurrentHeight / (float) CurrentWindowHeight; + + x = (int) Math.Round(x * scaleX); + y = (int) Math.Round(y * scaleY); + } + + public void ConvertCoordsGameToWin(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + var scaleX = CurrentWindowWidth / (float) CurrentWidth; + var scaleY = CurrentWindowHeight / (float) CurrentHeight; + + x = (int) Math.Round(x * scaleX); + y = (int) Math.Round(y * scaleY); + } + + public void ConvertCoordsGlobalToGame(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + var scaleX = CurrentWidth / (float) CurrentWindowWidth; + var scaleY = CurrentHeight / (float) CurrentWindowHeight; + + var p = new POINT(x, y); + + Service.CursorPosHooks.ScreenToClientOrig(_currentHwnd, &p); + + p.x = (int) Math.Round(p.x * scaleX); + p.y = (int) Math.Round(p.y * scaleY); + + Service.CursorPosHooks.ClientToScreenOrig(_currentHwnd, &p); + + x = p.x; + y = p.y; + } + + public void ConvertCoordsGameToGlobal(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + var scaleX = CurrentWindowWidth / (float) CurrentWidth; + var scaleY = CurrentWindowHeight / (float) CurrentHeight; + + var p = new POINT(x, y); + + Service.CursorPosHooks.ScreenToClientOrig(_currentHwnd, &p); + + p.x = (int) Math.Round(p.x * scaleX); + p.y = (int) Math.Round(p.y * scaleY); + + Service.CursorPosHooks.ClientToScreenOrig(_currentHwnd, &p); + + x = p.x; + y = p.y; + } + + public void Update() + { + bool unloading = Service.Plugin.Unloading; + + ref var cfg = ref Service.Config._.Display; + + var dev = (DeviceEx*) Device.Instance(); + var framework = Framework.Instance(); + var win = framework->GameWindow; + + _currentHwnd = (HWND) win->WindowHandle; + + // As a safety measure, don't mess with the structs if we're reading garbage. + // This isn't perfect, but it's better than nothing. + if (_currentHwnd != dev->hWnd) + { + if (!_currentHwndMismatch) + { + _currentHwndMismatch = true; + Service.PluginLog.Error($"HWND MISMATCH between GameWindow and Device: 0x{(long) (IntPtr) (_currentHwnd):X16} vs 0x{(long) dev->hWnd:X16}"); + Service.PluginLog.Info($"dev is at: 0x{(long) (IntPtr) dev:X16}"); + CheckHWND("GameWindow", _currentHwnd); + CheckHWND("Device", (HWND) dev->hWnd); + } + + return; + } + + _currentHwndMismatch = false; + + fixed (RECT* currentClientRectPtr = &_currentClientRect) + { + Service.WindowRectHooks.GetClientRectOrig(_currentHwnd, currentClientRectPtr); + } + + var rectWidth = _currentClientRect.right - _currentClientRect.left; + var rectHeight = _currentClientRect.bottom - _currentClientRect.top; + + if ((rectWidth <= 0 || rectHeight <= 0) && !unloading) + { + return; + } + + var enabled = !unloading && cfg.IsEnabled; + + uint width, height; + + if (cfg.IsScale || !enabled) + { + var scale = enabled ? cfg.Scale : 1f; + + width = (uint) Math.Round(rectWidth * scale); + height = (uint) Math.Round(rectHeight * scale); + } + else + { + width = cfg.Width; + height = cfg.Height; + } + + if (width < 256) + { + width = 256; + } + + if (height < 256) + { + height = 256; + } + + _requestedResolutionChange |= dev->RequestResolutionChange != 0; + + if (!_currentlyFakeResize && (unloading || enabled || _wasEnabled || _requestedResolutionChange) && + (width != dev->Width || height != dev->Height)) + { + Service.PluginLog.Info($"Changing graphics resolution from {dev->Width} x {dev->Height} to {width} x {height}"); + var mode = Service.DebugConfig.ForceSizeMode; + if (_requestedResolutionChange) + { + // Let's try to be cautious about other plugins (f.e. SimpleTweaks) changing the game resolution. + Service.PluginLog.Info($"Game resolution was changed externally - forcing resize via device, not window resize."); + mode = ForceSizeMode.LegacyRequestResolutionChange; + _requestedResolutionChange = false; + } + switch (mode) + { + case ForceSizeMode.Skip: + break; + case ForceSizeMode.LegacyRequestResolutionChange: + dev->NewWidth = width; + dev->NewHeight = height; + dev->RequestResolutionChange = 1; + Service.PluginLog.Info($"Changing game window from {win->WindowWidth} x {win->WindowHeight} to {width} x {height}"); + win->WindowWidth = (int) width; + win->WindowHeight = (int) height; + break; + case ForceSizeMode.FakeWindowResize: + var adjustedClientRect = new RECT(0, 0, rectWidth, rectHeight); + AdjustWindowRect(&adjustedClientRect, (uint) GetWindowLongPtr(_currentHwnd, GWL.GWL_STYLE), false); + var adjustedWidth = adjustedClientRect.right - adjustedClientRect.left; + var adjustedHeight = adjustedClientRect.bottom - adjustedClientRect.top; + Service.PluginLog.Info($"Resizing window to {adjustedWidth} x {adjustedHeight}"); + try + { + _currentlyFakeResize = true; + SendMessage(_currentHwnd, WM.WM_ENTERSIZEMOVE, 0, 0); + SendMessage(_currentHwnd, WM.WM_SIZE, SIZE_MAXSHOW, WndProcHook.SizeToParam((uint) adjustedWidth, (uint) adjustedHeight)); + SendMessage(_currentHwnd, WM.WM_PAINT, 0, 0); + SendMessage(_currentHwnd, WM.WM_EXITSIZEMOVE, 0, 0); + } + finally + { + _currentlyFakeResize = false; + } + break; + default: + Service.PluginLog.Error($"Unknown ForceSizeMode: {Service.DebugConfig.ForceSizeMode}"); + break; + } + } + + if (_wasEnabled != enabled) + { + Service.PluginLog.Info($"Changed state to: {enabled}"); + _wasEnabled = enabled; + } + + //Service.PluginLog.Debug($"NewWidth 0x{(long) (IntPtr) (&dev->NewWidth):X16}"); + //Service.PluginLog.Debug($"GameWindow->Width 0x{(long) (IntPtr) (&win->WindowWidth):X16}"); + + //Service.PluginLog.Info($"Game window at {win->WindowWidth} x {win->WindowHeight}"); + + //Service.PluginLog.Info($"AAA {Service.GameConfig.System.GetUInt(nameof(SystemConfigOption.UiHighScale))}"); + + CurrentBorderlessFullscreen = win->Borderless; + + if (Service.Config._.DXVKDWMHackMode != DXVKDWMHackMode.Off && !unloading) + { + SetDXVKDWMHack(Service.Config._.DXVKDWMHackMode); + } + else if (Service.Config._.DXVKDWMHackMode == DXVKDWMHackMode.Off && _currentDXVKDWMHackMode != DXVKDWMHackMode.Off) + { + SetDXVKDWMHack(DXVKDWMHackMode.Off); + } + + CurrentWidth = width; + CurrentHeight = height; + CurrentWindowWidth = (uint) rectWidth; + CurrentWindowHeight = (uint) rectHeight; + } + + private void SetDXVKDWMHack(DXVKDWMHackMode mode) + { + /* Default maximized style / exstyle is 0x95000000 / 0. + * WS.WS_POPUP | WS.WS_VISIBLE | WS.WS_CLIPSIBLINGS | WS.WS_MAXIMIZE + * Default windowed style / exstyle is 0x14CF0000 / 0. + * WS.WS_VISIBLE | WS.WS_CLIPSIBLINGS | WS.WS_CAPTION | WS.WS_SYSMENU | WS.WS_THICKFRAME | WS.WS_MINIMIZEBOX | WS.WS_MAXIMIZEBOX + */ + var styleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_STYLE); + var exstyleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE); + + var style = styleOrig; + var exstyle = exstyleOrig; + + var fullscreen = (style & WS.WS_SYSMENU) == 0; + + if (Service.DebugConfig.IsDebug) + { + Service.PluginLog.Info("--------"); + Service.PluginLog.Info($"STYLE: 0x{style:X8}"); + Service.PluginLog.Info($"EXSTYLE: 0x{GetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE):X8}"); + + Span name = stackalloc ushort[256]; + GetClassName(_currentHwnd, name.GetPointer(0), name.Length); + WNDCLASSEXW wce; + GetClassInfoEx(GetModuleHandle(null), name.GetPointer(0), &wce); + + Service.PluginLog.Info($"CLASS: {new string((char*) name.GetPointer(0))}"); + Service.PluginLog.Info($"CLASS.style: 0x{wce.style:X8}"); + } + + if (fullscreen) + { + if (mode == DXVKDWMHackMode.UnsetPopup) + { + style &= ~WS.WS_POPUP; + } + else + { + style |= WS.WS_POPUP; + } + } + + if (fullscreen && mode.IsSetClientEdge()) + { + exstyle |= WS.WS_EX_CLIENTEDGE; + exstyle |= WS.WS_EX_COMPOSITED; + } + else + { + exstyle &= ~(uint) WS.WS_EX_CLIENTEDGE; + exstyle &= ~(uint) WS.WS_EX_COMPOSITED; + } + + if (Service.DebugConfig.IsDebug) + { + Service.PluginLog.Info($"NEWSTYLE: 0x{style:X8}"); + Service.PluginLog.Info($"NEWEXSTYLE: 0x{exstyle:X8}"); + } + + if (style != styleOrig || exstyle != exstyleOrig || _currentDXVKDWMHackMode != mode) + { + if (Service.DebugConfig.IsDebug) + { + Service.PluginLog.Info("UPDATE"); + } + + SetWindowLong(_currentHwnd, GWL.GWL_STYLE, (int) style); + SetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE, (int) exstyle); + + SetWindowPos(_currentHwnd, HWND.NULL, 0, 0, 0, 0, SWP.SWP_NOZORDER | SWP.SWP_NOMOVE | SWP.SWP_NOSIZE | SWP.SWP_NOACTIVATE | SWP.SWP_DRAWFRAME); + ShowWindow(_currentHwnd, SW.SW_SHOW); + } + else if (Service.DebugConfig.IsDebug) + { + Service.PluginLog.Info("SAME"); + } + + _currentDXVKDWMHackMode = mode; + } + + private void CheckHWND(string from, HWND hwnd) + { + RECT rect = default; + if (!GetClientRect(hwnd, &rect)) + { + Service.PluginLog.Info($"{from} is sus: {Marshal.GetLastPInvokeErrorMessage()}"); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct DeviceEx + { + [FieldOffset(0)] + public Device _; + + public ref byte RequestResolutionChange => ref _.RequestResolutionChange; + + public ref uint Width => ref _.Width; + + public ref uint Height => ref _.Height; + +#if CRES_CLEAN || true + public ref void* hWnd => ref _.hWnd; + + public ref uint NewWidth => ref _.NewWidth; + + public ref uint NewHeight => ref _.NewHeight; +#else + // 7.2 adds 1B8 before hWnd (previously 820, now 9D8) + [FieldOffset(0x9D8)] + public unsafe void* hWnd; + + [FieldOffset(0x9D8 + 0x10)] + public uint NewWidth; + + [FieldOffset(0x9D8 + 0x14)] + public uint NewHeight; +#endif + } +} + +public enum ForceSizeMode +{ + Skip = -1, + + /// + /// Fake a window resize event. + /// + FakeWindowResize = 0, + + /// + /// Legacy mode, went through more initial testing, works well except for conflicts with other plugins. + /// Might revert or remove depending on how FakeWindowResize testing / fixups proceed. + /// + /// Update the graphics device width / height and set RequestResolutionChange. + /// This is the same method that other plugins such as SimpleTweaks or XIVWindowResizer use. + /// + LegacyRequestResolutionChange = 1 +} diff --git a/CustomResolution2782/GameSizeState.cs b/CustomResolution2782/GameSizeState.cs new file mode 100644 index 0000000..7530598 --- /dev/null +++ b/CustomResolution2782/GameSizeState.cs @@ -0,0 +1,149 @@ +using CustomResolution.Hooks; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Config; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Xml.Linq; +using TerraFX.Interop.Windows; +using static TerraFX.Interop.Windows.Windows; + +namespace CustomResolution; + +public unsafe class GameSizeState : IDisposable +{ + private bool _wasEnabled = false; + + private bool? _forceDynRezoConfig; + + public bool ConfigDynRezo { get; private set; } + public ResolutionScalingMode ConfigGraphicsRezoType { get; private set; } + public float ConfigGraphicsRezoScale { get; private set; } + + public void Dispose() + { + Service.Framework.RunOnFrameworkThread(Update); + } + + public void ForceDynRezoConfig(bool value) + { + _forceDynRezoConfig = value; + } + + public void Update() + { + ref var cfg = ref Service.Config._.Game; + + if (_forceDynRezoConfig.HasValue) + { + Service.GameConfig.System.Set(SystemConfigOption.DynamicRezoType.ToString(), _forceDynRezoConfig.Value ? 1U : 0U); + _forceDynRezoConfig = null; + } + + ConfigDynRezo = Service.GameConfig.System.GetUInt(SystemConfigOption.DynamicRezoType.ToString()) != 0U; + // GameConfig starts with FSR at 0; GraphicsConfig starts with FSR at 1 + ConfigGraphicsRezoType = (ResolutionScalingMode) (Service.GameConfig.System.GetUInt(SystemConfigOption.GraphicsRezoUpscaleType.ToString()) + 1); + ConfigGraphicsRezoScale = Service.GameConfig.System.GetUInt(SystemConfigOption.GraphicsRezoScale.ToString()) / 100f; + + var dev = Device.Instance(); + var gfx = GraphicsConfig.Instance(); + var rtm = (RenderTargetManagerEx*) RenderTargetManager.Instance(); + + //Service.PluginLog.Debug($"GraphicsRezoScale 0x{(long) (IntPtr) (&gfx->GraphicsRezoScale):X16}"); + //Service.PluginLog.Debug($"RTM 0x{(long) (IntPtr) rtm:X16}"); + + bool unloading = Service.Plugin.Unloading; + bool enabled = !unloading && cfg.IsEnabled; + + if (enabled || _wasEnabled) + { + gfx->GraphicsRezoScale = enabled ? cfg.Scale : ConfigGraphicsRezoScale; + gfx->DynamicRezoEnable = (byte) (ConfigDynRezo || enabled ? 1 : 0); + if (enabled) + { + gfx->GraphicsRezoUpscaleType = (byte) (cfg.Scale <= 1f ? Service.Config._.ResolutionScalingMode : ResolutionScalingMode.Fast); + } + else + { + gfx->GraphicsRezoUpscaleType = (byte) ConfigGraphicsRezoType; + } + _wasEnabled = enabled; + } + + if (enabled) + { + rtm->DynamicResolutionMinimumHeight = rtm->DynamicResolutionMaximumHeight; + } + + /* + rtm->Resolution_Width = (ushort) (dev->Width * gfx->GraphicsRezoScale); + rtm->Resolution_Height = (ushort) (dev->Height * gfx->GraphicsRezoScale); + rtm->DynamicResolutionActualTargetHeight = (ushort) (dev->Height * gfx->GraphicsRezoScale); + rtm->DynamicResolutionTargetHeight = (ushort) (dev->Height * gfx->GraphicsRezoScale); + rtm->DynamicResolutionMaximumHeight = (ushort) (dev->Height * gfx->GraphicsRezoScale); + rtm->DynamicResolutionMinimumHeight = (ushort) (dev->Height * gfx->GraphicsRezoScale); + */ + +#if false + Service.PluginLog.Debug($"DR {gfx->DynamicRezoEnable}"); + Service.PluginLog.Debug($"RTM {rtm->Resolution_Width} x {rtm->Resolution_Height}"); + Service.PluginLog.Debug($"RTM H {rtm->DynamicResolutionActualTargetHeight} {rtm->DynamicResolutionTargetHeight} {rtm->DynamicResolutionMaximumHeight} {rtm->DynamicResolutionMinimumHeight}"); + Service.PluginLog.Debug($"RTM S {rtm->GraphicsRezoScales[0]} {rtm->GraphicsRezoScales[1]} {rtm->GraphicsRezoScales[2]} {rtm->GraphicsRezoScales[3]} {rtm->GraphicsRezoScales[4]}"); +#endif + } + + // RenderTargetManager is updated very sporadically, and some fields we need flew out + // https://github.com/aers/FFXIVClientStructs/commit/589df2aa5cd9c98b4d62269034cd6da903f49b5f#diff-8e7d9b03cb91cb07a8d7b463b5be4672793a328703bde393e7acd890822a72cf + [StructLayout(LayoutKind.Explicit)] + private struct RenderTargetManagerEx + { + [FieldOffset(0)] + public RenderTargetManager _; + +#if CRES_CLEAN || false + public ref uint Resolution_Width => ref _.Resolution_Width; + public ref uint Resolution_Height => ref _.Resolution_Height; + public ref ushort DynamicResolutionActualTargetHeight => ref _.DynamicResolutionActualTargetHeight; + public ref ushort DynamicResolutionTargetHeight => ref _.DynamicResolutionTargetHeight; + public ref ushort DynamicResolutionMaximumHeight => ref _.DynamicResolutionMaximumHeight; + public ref ushort DynamicResolutionMinimumHeight => ref _.DynamicResolutionMinimumHeight; + public ref float GraphicsRezoScale => ref _.GraphicsRezoScale; +#else + [FieldOffset(0x428)] + public uint Resolution_Width; + [FieldOffset(0x428 + 0x4)] + public uint Resolution_Height; + + [FieldOffset(0x6F0 + 2 * 0)] + public ushort DynamicResolutionActualTargetHeight; + [FieldOffset(0x6F0 + 2 * 1)] + public ushort DynamicResolutionTargetHeight; + [FieldOffset(0x6F0 + 2 * 2)] + public ushort DynamicResolutionMaximumHeight; + [FieldOffset(0x6F0 + 2 * 3)] + public ushort DynamicResolutionMinimumHeight; + + public const int GraphicsRezoScalesCount = 5; + [FieldOffset(0x70C)] + public fixed float GraphicsRezoScales[GraphicsRezoScalesCount]; + [FieldOffset(0x70C + 4 * 0)] + public float GraphicsRezoScalePrev; + [FieldOffset(0x70C + 4 * 1)] + public float GraphicsRezoScaleGlassWidth; + [FieldOffset(0x70C + 4 * 2)] + public float GraphicsRezoScaleGlassHeight; + [FieldOffset(0x70C + 4 * 3)] + public float GraphicsRezoScaleUnk1; + [FieldOffset(0x70C + 4 * 4)] + public float GraphicsRezoScaleUnk2; +#endif + } +} diff --git a/CustomResolution2782/Hooks/CursorPosHooks.cs b/CustomResolution2782/Hooks/CursorPosHooks.cs index 4a8c27a..4b132c7 100644 --- a/CustomResolution2782/Hooks/CursorPosHooks.cs +++ b/CustomResolution2782/Hooks/CursorPosHooks.cs @@ -80,7 +80,7 @@ public sealed unsafe class CursorPosHooks : IDisposable Service.PluginLog.Debug($"GetCursorPos A @ {lpPoint->x} {lpPoint -> y}"); #endif - Service.Plugin.ConvertCoordsGlobalToGame(ref lpPoint->x, ref lpPoint->y); + Service.DisplaySize.ConvertCoordsGlobalToGame(ref lpPoint->x, ref lpPoint->y); #if false Service.PluginLog.Debug($"GetCursorPos B @ {lpPoint->x} {lpPoint->y}"); @@ -95,7 +95,7 @@ public sealed unsafe class CursorPosHooks : IDisposable Service.PluginLog.Debug($"SetCursorPos A @ {x} {y}"); #endif - Service.Plugin.ConvertCoordsGameToGlobal(ref x, ref y); + Service.DisplaySize.ConvertCoordsGameToGlobal(ref x, ref y); #if false Service.PluginLog.Debug($"SetCursorPos B @ {x} {y}"); diff --git a/CustomResolution2782/Hooks/WindowRectHooks.cs b/CustomResolution2782/Hooks/WindowRectHooks.cs index 1f96132..65a2409 100644 --- a/CustomResolution2782/Hooks/WindowRectHooks.cs +++ b/CustomResolution2782/Hooks/WindowRectHooks.cs @@ -50,9 +50,9 @@ public sealed unsafe class WindowRectHooks : IDisposable Service.PluginLog.Debug($"GetClientRectDetour A @ {lpRect->left} {lpRect->top} {lpRect->right} {lpRect->bottom}"); #endif - if (hWnd == Service.Plugin.CurrentHWND) + if (hWnd == Service.DisplaySize.CurrentHWND) { - Service.Plugin.ConvertCoordsWinToGame(ref lpRect->right, ref lpRect->bottom); + Service.DisplaySize.ConvertCoordsWinToGame(ref lpRect->right, ref lpRect->bottom); } #if false @@ -73,9 +73,9 @@ public sealed unsafe class WindowRectHooks : IDisposable Service.PluginLog.Debug($"SetWindowPosDetour A @ {X} {Y} {cx} {cy}"); #endif - if (Service.DebugConfig.SetWindowSizeMode == SetWindowSizeMode.LegacyHookSetWindowPos && hWnd == Service.Plugin.CurrentHWND) + if (Service.DebugConfig.SetWindowSizeMode == SetWindowSizeMode.LegacyHookSetWindowPos && hWnd == Service.DisplaySize.CurrentHWND) { - Service.Plugin.ConvertCoordsGameToWin(ref cx, ref cy); + Service.DisplaySize.ConvertCoordsGameToWin(ref cx, ref cy); } #if false diff --git a/CustomResolution2782/Hooks/WndProcHook.cs b/CustomResolution2782/Hooks/WndProcHook.cs index 2e6417d..31c2fd8 100644 --- a/CustomResolution2782/Hooks/WndProcHook.cs +++ b/CustomResolution2782/Hooks/WndProcHook.cs @@ -112,7 +112,7 @@ public sealed unsafe class WndProcHook : IDisposable } if (args.Message == WM.WM_WINDOWPOSCHANGING && - Service.Config.MinSizeMode == MinSizeMode.Unlocked) + Service.Config._.MinSizeMode == MinSizeMode.Unlocked) { args.SuppressCall = true; } @@ -127,8 +127,8 @@ public sealed unsafe class WndProcHook : IDisposable { ParamToCoords(args.LParam, out int x, out int y); Service.PluginLog.Debug($"WM_SIZE {x} x {y}"); - Service.Plugin.Update(); - args.LParam = SizeToParam(Service.Plugin.CurrentWidth, Service.Plugin.CurrentHeight); + Service.DisplaySize.Update(); + args.LParam = SizeToParam(Service.DisplaySize.CurrentWidth, Service.DisplaySize.CurrentHeight); } if (WM.WM_MOUSEFIRST <= args.Message && args.Message <= WM.WM_MOUSELAST) @@ -139,7 +139,7 @@ public sealed unsafe class WndProcHook : IDisposable Service.PluginLog.Debug($"WM_MOUSE A @ {x} {y}"); #endif - plugin.ConvertCoordsWinToGame(ref x, ref y); + Service.DisplaySize.ConvertCoordsWinToGame(ref x, ref y); #if false Service.PluginLog.Debug($"WM_MOUSE B @ {x} {y}"); @@ -148,8 +148,8 @@ public sealed unsafe class WndProcHook : IDisposable args.LParam = CoordsToParam(x, y); } - if (args.Message == WM.WM_NCCALCSIZE && args.WParam != 0 && plugin.CurrentBorderlessFullscreen && - Service.Config.DXVKDWMHackMode.IsSetClientEdge()) + if (args.Message == WM.WM_NCCALCSIZE && args.WParam != 0 && Service.DisplaySize.CurrentBorderlessFullscreen && + Service.Config._.DXVKDWMHackMode.IsSetClientEdge()) { var ncsize = (NCCALCSIZE_PARAMS*) args.LParam; MONITORINFO monitorInfo = new() @@ -157,12 +157,12 @@ public sealed unsafe class WndProcHook : IDisposable cbSize = (uint) sizeof(MONITORINFO) }; - if (MonitorFromWindow(plugin.CurrentHWND, MONITOR.MONITOR_DEFAULTTONEAREST) is { } monitor && monitor != HMONITOR.NULL && + if (MonitorFromWindow(Service.DisplaySize.CurrentHWND, MONITOR.MONITOR_DEFAULTTONEAREST) is { } monitor && monitor != HMONITOR.NULL && GetMonitorInfo(monitor, &monitorInfo)) { ncsize->rgrc[0] = monitorInfo.rcMonitor; - switch (Service.Config.DXVKDWMHackMode) + switch (Service.Config._.DXVKDWMHackMode) { case DXVKDWMHackMode.SetClientEdgeResize: ncsize->rgrc[0].bottom += 1; diff --git a/CustomResolution2782/Plugin.cs b/CustomResolution2782/Plugin.cs index c83a862..92d18a3 100644 --- a/CustomResolution2782/Plugin.cs +++ b/CustomResolution2782/Plugin.cs @@ -4,6 +4,7 @@ using Dalamud.Game.Config; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.Interop; using ImGuiNET; @@ -11,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Xml.Linq; using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -20,28 +22,18 @@ public sealed unsafe class Plugin : IDalamudPlugin { private object _disposeLock = new(); - private readonly HRGN _invisibleRgn; - private readonly List _cmds; - private bool _unloading = false; - private bool _wasEnabled = false; - private HWND _currentHwnd; - private bool _currentHwndMismatch = false; - private RECT _currentClientRect; - private DXVKDWMHackMode _currentDXVKDWMHackMode = DXVKDWMHackMode.Off; private bool _ignoreConfigChanges = false; - private bool _currentlyFakeResize = false; - private bool _requestedResolutionChange = false; public Plugin(IDalamudPluginInterface pluginInterface) { - _invisibleRgn = CreateRectRgn(0, 0, -1, -1); - pluginInterface.Create(); Service.Plugin = this; - Service.DebugConfig = new DebugConfiguration(); + Service.DebugConfig = new(); + Service.DisplaySize = new(); + Service.GameSize = new(); Service.Config = Service.PluginInterface.GetPluginConfig() as Configuration ?? new(); Service.Config.Initialize(Service.PluginInterface); @@ -68,25 +60,18 @@ public sealed unsafe class Plugin : IDalamudPlugin public string Name => "CustomResolution"; - public HWND CurrentHWND => _currentHwnd; - - public uint CurrentWidth { get; private set; } - public uint CurrentHeight { get; private set; } - public uint CurrentWindowWidth { get; private set; } - public uint CurrentWindowHeight { get; private set; } - - public bool CurrentBorderlessFullscreen { get; private set; } + public bool Unloading { get; private set; } = false; public void Dispose() { - _unloading = true; + Unloading = true; lock (_disposeLock) { Service.Framework.Update -= OnFrameworkUpdate; Service.GameConfig.SystemChanged -= OnSystemConfigChanged; - - Service.Framework.RunOnFrameworkThread(Update); + Service.DisplaySize.Dispose(); + Service.GameSize.Dispose(); } foreach (Cmd cmd in _cmds) @@ -103,326 +88,23 @@ public sealed unsafe class Plugin : IDalamudPlugin Service.Plugin = null!; } - public void ConvertCoordsWinToGame(ref int x, ref int y) + private void OnFrameworkUpdateForSize(ref ConfigurationV1.SizeConfig size) { - if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) - { - return; - } + var io = ImGui.GetIO(); - var scaleX = CurrentWidth / (float) CurrentWindowWidth; - var scaleY = CurrentHeight / (float) CurrentWindowHeight; - - x = (int) Math.Round(x * scaleX); - y = (int) Math.Round(y * scaleY); - } - - public void ConvertCoordsGameToWin(ref int x, ref int y) - { - if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) - { - return; - } - - var scaleX = CurrentWindowWidth / (float) CurrentWidth; - var scaleY = CurrentWindowHeight / (float) CurrentHeight; - - x = (int) Math.Round(x * scaleX); - y = (int) Math.Round(y * scaleY); - } - - public void ConvertCoordsGlobalToGame(ref int x, ref int y) - { - if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) - { - return; - } - - var scaleX = CurrentWidth / (float) CurrentWindowWidth; - var scaleY = CurrentHeight / (float) CurrentWindowHeight; - - var p = new POINT(x, y); - - Service.CursorPosHooks.ScreenToClientOrig(_currentHwnd, &p); - - p.x = (int) Math.Round(p.x * scaleX); - p.y = (int) Math.Round(p.y * scaleY); - - Service.CursorPosHooks.ClientToScreenOrig(_currentHwnd, &p); - - x = p.x; - y = p.y; - } - - public void ConvertCoordsGameToGlobal(ref int x, ref int y) - { - if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) - { - return; - } - - var scaleX = CurrentWindowWidth / (float) CurrentWidth; - var scaleY = CurrentWindowHeight / (float) CurrentHeight; - - var p = new POINT(x, y); - - Service.CursorPosHooks.ScreenToClientOrig(_currentHwnd, &p); - - p.x = (int) Math.Round(p.x * scaleX); - p.y = (int) Math.Round(p.y * scaleY); - - Service.CursorPosHooks.ClientToScreenOrig(_currentHwnd, &p); - - x = p.x; - y = p.y; - } - - public void Update() - { - var dev = Device.Instance(); - var framework = Framework.Instance(); - var win = framework->GameWindow; - - _currentHwnd = (HWND) win->WindowHandle; - -#if CRES_CLEAN || true - var _dev_hWnd = (IntPtr*) &dev->hWnd; - var _dev_NewWidth = &dev->NewWidth; - var _dev_NewHeight = &dev->NewHeight; - var _dev_RequestResolutionChange = &dev->RequestResolutionChange; -#else - // 7.2 adds 1B8 before hWnd (previously 820, now 9D8) - var _dev_hWnd = (nint*) ((IntPtr) dev + 0x9D8); - var _dev_NewWidth = (uint*) ((IntPtr) dev + 0x9D8 + 0x10); - var _dev_NewHeight = (uint*) ((IntPtr) dev + 0x9D8 + 0x14); - var _dev_RequestResolutionChange = &dev->RequestResolutionChange; -#endif - - // As a safety measure, don't mess with the structs if we're reading garbage. - // This isn't perfect, but it's better than nothing. - if (_currentHwnd != *_dev_hWnd) - { - if (!_currentHwndMismatch) + if (size.HotkeyKey != VirtualKey.NO_KEY && + (ImGuiNative.igIsKeyPressed((ImGuiKey) size.HotkeyKey, 0) != 0 || Service.KeyState.GetRawValue(size.HotkeyKey) != 0) && + (size.HotkeyModifier switch { - _currentHwndMismatch = true; - Service.PluginLog.Error($"HWND MISMATCH between GameWindow and Device: 0x{(long) (IntPtr) (_currentHwnd):X16} vs 0x{(long) *_dev_hWnd:X16}"); - Service.PluginLog.Info($"dev is at: 0x{(long) (IntPtr) dev:X16}"); - CheckHWND("GameWindow", _currentHwnd); - CheckHWND("Device", (HWND) (*_dev_hWnd)); - } - - return; - } - - _currentHwndMismatch = false; - - fixed (RECT* currentClientRectPtr = &_currentClientRect) + ModifierKey.NONE => !io.KeyCtrl && !io.KeyAlt && !io.KeyShift, + ModifierKey.CTRL => io.KeyCtrl && !io.KeyAlt && !io.KeyShift, + ModifierKey.ALT => !io.KeyCtrl && io.KeyAlt && !io.KeyShift, + ModifierKey.SHIFT => !io.KeyCtrl && !io.KeyAlt && io.KeyShift, + _ => false, + })) { - Service.WindowRectHooks.GetClientRectOrig(_currentHwnd, currentClientRectPtr); - } - - var rectWidth = _currentClientRect.right - _currentClientRect.left; - var rectHeight = _currentClientRect.bottom - _currentClientRect.top; - - if ((rectWidth <= 0 || rectHeight <= 0) && !_unloading) - { - return; - } - - var enabled = !_unloading && Service.Config.IsEnabled; - - uint width, height; - - if (Service.Config.IsScale || !enabled) - { - var scale = enabled ? Service.Config.Scale : 1f; - - width = (uint) Math.Round(rectWidth * scale); - height = (uint) Math.Round(rectHeight * scale); - } - else - { - width = Service.Config.Width; - height = Service.Config.Height; - } - - if (width < 256) - { - width = 256; - } - - if (height < 256) - { - height = 256; - } - - _requestedResolutionChange |= (*_dev_RequestResolutionChange) != 0; - - if (!_currentlyFakeResize && (_unloading || enabled || _wasEnabled || _requestedResolutionChange) && - (width != dev->Width || height != dev->Height)) - { - Service.PluginLog.Info($"Changing graphics resolution from {dev->Width} x {dev->Height} to {width} x {height}"); - var mode = Service.DebugConfig.ForceSizeMode; - if (_requestedResolutionChange) - { - // Let's try to be cautious about other plugins (f.e. SimpleTweaks) changing the game resolution. - Service.PluginLog.Info($"Game resolution was changed externally - forcing resize via device, not window resize."); - mode = ForceSizeMode.LegacyRequestResolutionChange; - _requestedResolutionChange = false; - } - switch (mode) - { - case ForceSizeMode.Skip: - break; - case ForceSizeMode.LegacyRequestResolutionChange: - *_dev_NewWidth = width; - *_dev_NewHeight = height; - *_dev_RequestResolutionChange = 1; - Service.PluginLog.Info($"Changing game window from {win->WindowWidth} x {win->WindowHeight} to {width} x {height}"); - win->WindowWidth = (int) width; - win->WindowHeight = (int) height; - break; - case ForceSizeMode.FakeWindowResize: - var adjustedClientRect = new RECT(0, 0, rectWidth, rectHeight); - AdjustWindowRect(&adjustedClientRect, (uint) GetWindowLongPtr(_currentHwnd, GWL.GWL_STYLE), false); - var adjustedWidth = adjustedClientRect.right - adjustedClientRect.left; - var adjustedHeight = adjustedClientRect.bottom - adjustedClientRect.top; - Service.PluginLog.Info($"Resizing window to {adjustedWidth} x {adjustedHeight}"); - try - { - _currentlyFakeResize = true; - SendMessage(_currentHwnd, WM.WM_ENTERSIZEMOVE, 0, 0); - SendMessage(_currentHwnd, WM.WM_SIZE, SIZE_MAXSHOW, WndProcHook.SizeToParam((uint) adjustedWidth, (uint) adjustedHeight)); - SendMessage(_currentHwnd, WM.WM_PAINT, 0, 0); - SendMessage(_currentHwnd, WM.WM_EXITSIZEMOVE, 0, 0); - } - finally - { - _currentlyFakeResize = false; - } - break; - default: - Service.PluginLog.Error($"Unknown ForceSizeMode: {Service.DebugConfig.ForceSizeMode}"); - break; - } - } - - if (_wasEnabled != enabled) - { - Service.PluginLog.Info($"Changed state to: {enabled}"); - _wasEnabled = enabled; - } - - //Service.PluginLog.Debug($"NewWidth 0x{(long) (IntPtr) (&dev->NewWidth):X16}"); - //Service.PluginLog.Debug($"GameWindow->Width 0x{(long) (IntPtr) (&win->WindowWidth):X16}"); - - //Service.PluginLog.Info($"Game window at {win->WindowWidth} x {win->WindowHeight}"); - - //Service.PluginLog.Info($"AAA {Service.GameConfig.System.GetUInt(nameof(SystemConfigOption.UiHighScale))}"); - - CurrentBorderlessFullscreen = win->Borderless; - - if (Service.Config.DXVKDWMHackMode != DXVKDWMHackMode.Off && !_unloading) - { - SetDXVKDWMHack(Service.Config.DXVKDWMHackMode); - } - else if (Service.Config.DXVKDWMHackMode == DXVKDWMHackMode.Off && _currentDXVKDWMHackMode != DXVKDWMHackMode.Off) - { - SetDXVKDWMHack(DXVKDWMHackMode.Off); - } - - CurrentWidth = width; - CurrentHeight = height; - CurrentWindowWidth = (uint) rectWidth; - CurrentWindowHeight = (uint) rectHeight; - } - - private void SetDXVKDWMHack(DXVKDWMHackMode mode) - { - /* Default maximized style / exstyle is 0x95000000 / 0. - * WS.WS_POPUP | WS.WS_VISIBLE | WS.WS_CLIPSIBLINGS | WS.WS_MAXIMIZE - * Default windowed style / exstyle is 0x14CF0000 / 0. - * WS.WS_VISIBLE | WS.WS_CLIPSIBLINGS | WS.WS_CAPTION | WS.WS_SYSMENU | WS.WS_THICKFRAME | WS.WS_MINIMIZEBOX | WS.WS_MAXIMIZEBOX - */ - var styleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_STYLE); - var exstyleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE); - - var style = styleOrig; - var exstyle = exstyleOrig; - - var fullscreen = (style & WS.WS_SYSMENU) == 0; - - if (Service.DebugConfig.IsDebug) - { - Service.PluginLog.Info("--------"); - Service.PluginLog.Info($"STYLE: 0x{style:X8}"); - Service.PluginLog.Info($"EXSTYLE: 0x{GetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE):X8}"); - - Span name = stackalloc ushort[256]; - GetClassName(_currentHwnd, name.GetPointer(0), name.Length); - WNDCLASSEXW wce; - GetClassInfoEx(GetModuleHandle(null), name.GetPointer(0), &wce); - - Service.PluginLog.Info($"CLASS: {new string((char*) name.GetPointer(0))}"); - Service.PluginLog.Info($"CLASS.style: 0x{wce.style:X8}"); - } - - if (fullscreen) - { - if (mode == DXVKDWMHackMode.UnsetPopup) - { - style &= ~WS.WS_POPUP; - } - else - { - style |= WS.WS_POPUP; - } - } - - if (fullscreen && mode.IsSetClientEdge()) - { - exstyle |= WS.WS_EX_CLIENTEDGE; - exstyle |= WS.WS_EX_COMPOSITED; - } - else - { - exstyle &= ~(uint) WS.WS_EX_CLIENTEDGE; - exstyle &= ~(uint) WS.WS_EX_COMPOSITED; - } - - if (Service.DebugConfig.IsDebug) - { - Service.PluginLog.Info($"NEWSTYLE: 0x{style:X8}"); - Service.PluginLog.Info($"NEWEXSTYLE: 0x{exstyle:X8}"); - } - - if (style != styleOrig || exstyle != exstyleOrig || _currentDXVKDWMHackMode != mode) - { - if (Service.DebugConfig.IsDebug) - { - Service.PluginLog.Info("UPDATE"); - } - - SetWindowLong(_currentHwnd, GWL.GWL_STYLE, (int) style); - SetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE, (int) exstyle); - - SetWindowPos(_currentHwnd, HWND.NULL, 0, 0, 0, 0, SWP.SWP_NOZORDER | SWP.SWP_NOMOVE | SWP.SWP_NOSIZE | SWP.SWP_NOACTIVATE | SWP.SWP_DRAWFRAME); - ShowWindow(_currentHwnd, SW.SW_SHOW); - } - else if (Service.DebugConfig.IsDebug) - { - Service.PluginLog.Info("SAME"); - } - - _currentDXVKDWMHackMode = mode; - } - - private void CheckHWND(string from, HWND hwnd) - { - RECT rect = default; - if (!GetClientRect(hwnd, &rect)) - { - Service.PluginLog.Info($"{from} is sus: {Marshal.GetLastPInvokeErrorMessage()}"); + size.IsEnabled = !size.IsEnabled; + Service.Config.Save(); } } @@ -430,24 +112,11 @@ public sealed unsafe class Plugin : IDalamudPlugin { lock (_disposeLock) { - var io = ImGui.GetIO(); + OnFrameworkUpdateForSize(ref Service.Config._.Display); + OnFrameworkUpdateForSize(ref Service.Config._.Game); - if (Service.Config.HotkeyKey != VirtualKey.NO_KEY && - (ImGuiNative.igIsKeyPressed((ImGuiKey) Service.Config.HotkeyKey, 0) != 0 || Service.KeyState.GetRawValue(Service.Config.HotkeyKey) != 0) && - (Service.Config.HotkeyModifier switch - { - ModifierKey.NONE => !io.KeyCtrl && !io.KeyAlt && !io.KeyShift, - ModifierKey.CTRL => io.KeyCtrl && !io.KeyAlt && !io.KeyShift, - ModifierKey.ALT => !io.KeyCtrl && io.KeyAlt && !io.KeyShift, - ModifierKey.SHIFT => !io.KeyCtrl && !io.KeyAlt && io.KeyShift, - _ => false, - })) - { - Service.Config.IsEnabled = !Service.Config.IsEnabled; - Service.Config.Save(); - } - - Update(); + Service.DisplaySize.Update(); + Service.GameSize.Update(); } } @@ -469,11 +138,12 @@ public sealed unsafe class Plugin : IDalamudPlugin case SystemConfigOption.ScreenHeight: if (Service.DebugConfig.SetWindowSizeMode == SetWindowSizeMode.InterceptSystemConfig) { + var d = Service.DisplaySize; var name = e.ConfigOption.ToString(); var valueOrig = Service.GameConfig.System.GetUInt(name); var isW = e.ConfigOption == SystemConfigOption.ScreenWidth; - var scale = (isW ? CurrentWindowWidth : CurrentWindowHeight) / (float) (isW ? CurrentWidth : CurrentHeight); + var scale = (isW ? d.CurrentWindowWidth : d.CurrentWindowHeight) / (float) (isW ? d.CurrentWidth : d.CurrentHeight); var valueNew = (uint) Math.Round(valueOrig * scale); Service.PluginLog.Info($"Intercepting config value change for {name}: {valueOrig} -> {valueNew}"); @@ -492,22 +162,3 @@ public sealed unsafe class Plugin : IDalamudPlugin } } } - -public enum ForceSizeMode -{ - Skip = -1, - - /// - /// Fake a window resize event. - /// - FakeWindowResize = 0, - - /// - /// Legacy mode, went through more initial testing, works well except for conflicts with other plugins. - /// Might revert or remove depending on how FakeWindowResize testing / fixups proceed. - /// - /// Update the graphics device width / height and set RequestResolutionChange. - /// This is the same method that other plugins such as SimpleTweaks or XIVWindowResizer use. - /// - LegacyRequestResolutionChange = 1 -} diff --git a/CustomResolution2782/Service.cs b/CustomResolution2782/Service.cs index ca29b59..6c45e7c 100644 --- a/CustomResolution2782/Service.cs +++ b/CustomResolution2782/Service.cs @@ -10,6 +10,9 @@ public sealed class Service { public static Plugin Plugin { get; internal set; } = null!; + public static DisplaySizeState DisplaySize { get; internal set; } = null!; + public static GameSizeState GameSize { get; internal set; } = null!; + public static Configuration Config { get; internal set; } = null!; public static DebugConfiguration DebugConfig { get; internal set; } = null!; diff --git a/CustomResolution2782/Windows/ConfigWindow.cs b/CustomResolution2782/Windows/ConfigWindow.cs index 04b1631..60643fa 100644 --- a/CustomResolution2782/Windows/ConfigWindow.cs +++ b/CustomResolution2782/Windows/ConfigWindow.cs @@ -1,9 +1,12 @@ using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using ImGuiNET; using System; using System.Linq; using System.Numerics; +using System.Xml.Linq; +using TerraFX.Interop.Windows; namespace CustomResolution.Windows; @@ -11,17 +14,10 @@ public class ConfigWindow : Window, IDisposable { private VirtualKey[] _validKeys = new VirtualKey[] { VirtualKey.NO_KEY }.Union(Service.KeyState.GetValidVirtualKeys()).ToArray(); - private int[] _displayCurrentWH = new int[2]; - private int[] _displayCurrentWindowWH = new int[2]; + private readonly int[] _displayCurrentWH = new int[2]; + private readonly int[] _displayCurrentWindowWH = new int[2]; - private bool _configIsEnabled; - private VirtualKey _configHotkeyKey; - private ModifierKey _configHotkeyModifier; - private bool _configIsScale; - private float _configScale; - private int[] _configWH = new int[2]; - private DXVKDWMHackMode _configDXVKDWMHackMode; - private MinSizeMode _configMinSizeMode; + private ConfigurationV1 _; public ConfigWindow() : base( "CustomResolution", @@ -36,65 +32,152 @@ public class ConfigWindow : Window, IDisposable public void UpdateFromConfig() { - var config = Service.Config; - - _configIsEnabled = config.IsEnabled; - _configHotkeyKey = config.HotkeyKey; - _configHotkeyModifier = config.HotkeyModifier; - _configIsScale = config.IsScale; - _configScale = config.Scale; - _configWH[0] = (int) config.Width; - _configWH[1] = (int) config.Height; - _configDXVKDWMHackMode = config.DXVKDWMHackMode; - _configMinSizeMode = config.MinSizeMode; + _ = Service.Config._; } public void UpdateToConfig() { - var config = Service.Config; - - config.IsEnabled = _configIsEnabled; - config.HotkeyKey = _configHotkeyKey; - config.HotkeyModifier = _configHotkeyModifier; - config.IsScale = _configIsScale; - config.Scale = _configScale; - config.Width = (uint) _configWH[0]; - config.Height = (uint) _configWH[1]; - config.DXVKDWMHackMode = _configDXVKDWMHackMode; - config.MinSizeMode = _configMinSizeMode; - - config.Save(); + Service.Config._ = _; + Service.Config.Save(); } public override void Draw() { - var plugin = Service.Plugin; var save = false; - _displayCurrentWH[0] = (int) plugin.CurrentWidth; - _displayCurrentWH[1] = (int) plugin.CurrentHeight; - _displayCurrentWindowWH[0] = (int) plugin.CurrentWindowWidth; - _displayCurrentWindowWH[1] = (int) plugin.CurrentWindowHeight; + _displayCurrentWH[0] = (int) Service.DisplaySize.CurrentWidth; + _displayCurrentWH[1] = (int) Service.DisplaySize.CurrentHeight; + _displayCurrentWindowWH[0] = (int) Service.DisplaySize.CurrentWindowWidth; + _displayCurrentWindowWH[1] = (int) Service.DisplaySize.CurrentWindowHeight; ImGui.BeginDisabled(); ImGui.InputInt2("Current window size", ref _displayCurrentWindowWH[0]); - ImGui.InputInt2("Current render size", ref _displayCurrentWH[0]); + ImGui.InputInt2("Current display size", ref _displayCurrentWH[0]); ImGui.EndDisabled(); - ImGui.Checkbox("Enabled", ref _configIsEnabled); + using (var imTabBar = ImRaii.TabBar("MalfunctionMainTabs")) + { + DrawDisplayTab(); + + DrawGameTab(); + + DrawCommonTab(); + } + + if (ImGui.Button("Save and apply")) + { + save = true; + } + + if (save) + { + UpdateToConfig(); + } + } + + private void DrawDisplayTab() + { + using var imTab = ImRaii.TabItem($"Display##DisplayTab"); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(@"The display resolution affects everything, +including screenshots taken in-game and via ReShade. + +This works best with the ""High Resolution UI Settings"" +in the system configuration, as it also affects the UI. + +In general: Use it as a fake fullscreen / window resolution, +best suited for screenshots. +Changed via /cres"); + } + + if (!imTab.Success) + { + return; + } + + DrawSizeTabContents(ref _.Display, "Display"); + } + + private void DrawGameTab() + { + using var imTab = ImRaii.TabItem($"Gameplay##GameTab"); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(@"The gameplay resolution affects only the quality of your character +and everything around them, leaving the HUD untouched. +This also doesn't touch the screenshot resolution. + +This works as an advanced version of the in-game ""3D Resolution Scaling"" graphics options, +allowing you to go smaller than 0.5x for more FPS, or higher than 1x for higher quality. + +This overrides dynamic resolution! + +In general: Use it for gameplay, not for screenshots. +Changed via /gres"); + } + + if (!imTab.Success) + { + return; + } + +#if false + if (Service.GameSize.CurrentDynRezoConfig) + { + using (ImRaii.PushColor(ImGuiCol.Border, 0xff00ffff)) + { + if (ImGui.Button(@"This might not work properly while in-game dynamic resolution is enabled! +Click here to disable it.")) + { + Service.GameSize.ForceDynRezoConfig(false); + } + + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight() * 0.1f)); + } + } +#endif + + DrawSizeTabContents(ref _.Game, "Game", scaleOnly: true); + + if (ImGui.BeginCombo("Graphics Upscaling", _.ResolutionScalingMode.ToHumanNameString())) + { + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToHumanNameString(), _.ResolutionScalingMode == mode, mode.IsUnsupported() ? ImGuiSelectableFlags.Disabled : ImGuiSelectableFlags.None)) + { + _.ResolutionScalingMode = mode; + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && mode.ToHumanInfoString() is { } info) + { + ImGui.SetTooltip(info); + } + } + + ImGui.EndCombo(); + } + } + + + private void DrawSizeTabContents(ref ConfigurationV1.SizeConfig size, string name, bool scaleOnly = false) + { + ImGui.Checkbox("Enabled", ref size.IsEnabled); ImGui.SameLine(); ImGui.Dummy(new Vector2(20, 0)); ImGui.SameLine(); ImGui.SetNextItemWidth(80); - if (ImGui.BeginCombo("##_configHotkeyModifier", _configHotkeyModifier.ToHumanNameString())) + if (ImGui.BeginCombo($"##_{name}_configHotkeyModifier", size.HotkeyModifier.ToHumanNameString())) { foreach (var key in Enum.GetValues()) { - if (ImGui.Selectable(key.ToHumanNameString(), _configHotkeyModifier == key, ImGuiSelectableFlags.None)) + if (ImGui.Selectable(key.ToHumanNameString(), size.HotkeyModifier == key, ImGuiSelectableFlags.None)) { - _configHotkeyModifier = key; + size.HotkeyModifier = key; } if (ImGui.IsItemHovered() && key.ToHumanInfoString() is { } info) @@ -108,13 +191,13 @@ public class ConfigWindow : Window, IDisposable ImGui.SameLine(); ImGui.SetNextItemWidth(160); - if (ImGui.BeginCombo("##_configHotkeyKey", _configHotkeyKey.ToHumanNameString())) + if (ImGui.BeginCombo($"##_{name}_configHotkeyKey", size.HotkeyKey.ToHumanNameString())) { foreach (var key in _validKeys) { - if (ImGui.Selectable(key.ToHumanNameString(), _configHotkeyKey == key, ImGuiSelectableFlags.None)) + if (ImGui.Selectable(key.ToHumanNameString(), size.HotkeyKey == key, ImGuiSelectableFlags.None)) { - _configHotkeyKey = key; + size.HotkeyKey = key; } if (ImGui.IsItemHovered() && key.ToHumanInfoString() is { } info) @@ -130,34 +213,27 @@ public class ConfigWindow : Window, IDisposable ImGui.Text("Hotkey"); - ImGui.Checkbox("Use scale", ref _configIsScale); - - if (_configIsScale) + if (!scaleOnly) { - ImGui.InputFloat("Scale", ref _configScale, 0.01f, 0.1f, "%.3f", ImGuiInputTextFlags.EnterReturnsTrue); + ImGui.Checkbox("Use scale", ref size.IsScale); + } + + if (size.IsScale || scaleOnly) + { + ImGui.InputFloat("Scale", ref size.Scale, 0.01f, 0.1f, "%.3f", ImGuiInputTextFlags.EnterReturnsTrue); } else { - ImGui.InputInt2("Size in pixels", ref _configWH[0]); - } - - if (ImGui.BeginCombo("Borderless window workaround", _configDXVKDWMHackMode.ToHumanNameString())) - { - foreach (var mode in Enum.GetValues()) + unsafe { - if (ImGui.Selectable(mode.ToHumanNameString(), _configDXVKDWMHackMode == mode, ImGuiSelectableFlags.None)) - { - _configDXVKDWMHackMode = mode; - } - - if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info) - { - ImGui.SetTooltip(info); - } + ImGui.InputInt2("Size in pixels", ref size.WH[0]); } - - ImGui.EndCombo(); } + } + + private void DrawCommonTab() + { + using var imTab = ImRaii.TabItem($"Common##CommonTab"); if (ImGui.IsItemHovered()) { @@ -172,13 +248,18 @@ Works even with the scaling above disabled. Not intended to be used with proper fullscreen."); } - if (ImGui.BeginCombo("Minimum window size", _configMinSizeMode.ToHumanNameString())) + if (!imTab.Success) { - foreach (var mode in Enum.GetValues()) + return; + } + + if (ImGui.BeginCombo("Borderless window workaround", _.DXVKDWMHackMode.ToHumanNameString())) + { + foreach (var mode in Enum.GetValues()) { - if (ImGui.Selectable(mode.ToHumanNameString(), _configMinSizeMode == mode, ImGuiSelectableFlags.None)) + if (ImGui.Selectable(mode.ToHumanNameString(), _.DXVKDWMHackMode == mode, ImGuiSelectableFlags.None)) { - _configMinSizeMode = mode; + _.DXVKDWMHackMode = mode; } if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info) @@ -190,14 +271,28 @@ Not intended to be used with proper fullscreen."); ImGui.EndCombo(); } - if (ImGui.Button("Save and apply")) + if (ImGui.IsItemHovered()) { - save = true; + ImGui.SetTooltip(@"Smaller tweaks and fixes, f.e. for DXVK on Windows, +or to allow making the window smaller than 1024x768."); } - if (save) + if (ImGui.BeginCombo("Minimum window size", _.MinSizeMode.ToHumanNameString())) { - UpdateToConfig(); + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToHumanNameString(), _.MinSizeMode == mode, ImGuiSelectableFlags.None)) + { + _.MinSizeMode = mode; + } + + if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info) + { + ImGui.SetTooltip(info); + } + } + + ImGui.EndCombo(); } } }