From c5feafcb2810ff5cdb523cb8880874dc6546ae30 Mon Sep 17 00:00:00 2001 From: Jade Macho Date: Sat, 16 Mar 2024 21:29:16 +0100 Subject: [PATCH] Allow switching between different borderless window hacks --- .../Cmds/BorderlessWindowedCmd.cs | 47 ------- CustomResolution/Cmds/MainCmd.cs | 15 +++ CustomResolution/Configuration.cs | 28 ++++- CustomResolution/CustomResolution.csproj | 2 +- CustomResolution/Hooks/WndProcHook.cs | 39 ++++-- CustomResolution/Plugin.cs | 116 ++++++++++++------ CustomResolution/Windows/ConfigWindow.cs | 35 ++++-- 7 files changed, 177 insertions(+), 105 deletions(-) delete mode 100644 CustomResolution/Cmds/BorderlessWindowedCmd.cs diff --git a/CustomResolution/Cmds/BorderlessWindowedCmd.cs b/CustomResolution/Cmds/BorderlessWindowedCmd.cs deleted file mode 100644 index d92df77..0000000 --- a/CustomResolution/Cmds/BorderlessWindowedCmd.cs +++ /dev/null @@ -1,47 +0,0 @@ -using static FFXIVClientStructs.FFXIV.Client.UI.AddonRelicNoteBook; -using System.Collections; -using System.Globalization; -using System; - -namespace CustomResolution.Cmds; - -public sealed class BorderlessWindowedCmd : Cmd -{ - public override string Name => "cresbw"; - - public override string HelpMessage => $"Tweak the \"Apply borderless window workaround\" toggle.\n" + - $"\tExamples:\n" + - $"\tTo enable / disable it:\n\t\t{FullName} on\n\t\t{FullName} off\n\t\t{FullName} toggle"; - - public override void Run(string arguments) - { - if (string.IsNullOrEmpty(arguments)) - { - Service.PrintChat("Invalid parameters."); - return; - } - - switch (arguments.ToLowerInvariant()) - { - case "on": - Service.Config.IsDXVKDWMHackEnabled = true; - Service.Config.Save(); - Service.PrintChat("Enabled borderless window workaround."); - return; - - case "off": - Service.Config.IsDXVKDWMHackEnabled = false; - Service.Config.Save(); - Service.PrintChat("Disabled borderless window workaround."); - return; - - case "toggle": - Service.Config.IsDXVKDWMHackEnabled = !Service.Config.IsDXVKDWMHackEnabled; - Service.Config.Save(); - Service.PrintChat($"{(Service.Config.IsDXVKDWMHackEnabled ? "Enabled" : "Disabled")} borderless window workaround."); - return; - } - - Service.PrintChat("Invalid parameters."); - } -} diff --git a/CustomResolution/Cmds/MainCmd.cs b/CustomResolution/Cmds/MainCmd.cs index b8ba7be..f28868b 100644 --- a/CustomResolution/Cmds/MainCmd.cs +++ b/CustomResolution/Cmds/MainCmd.cs @@ -64,6 +64,21 @@ public sealed class MainCmd : Cmd Service.Config.Save(); Service.PrintChat($"{(Service.Config.IsEnabled ? "Enabled" : "Disabled")} custom resolution."); return; + + case "debugon": + Service.Plugin.IsDebug = true; + Service.PrintChat("Enabled cres debug."); + return; + + case "debugoff": + Service.Plugin.IsDebug = false; + Service.PrintChat("Disabled cres debug."); + return; + + case "debug": + Service.Plugin.IsDebug = !Service.Plugin.IsDebug; + Service.PrintChat($"{(Service.Plugin.IsDebug ? "Enabled" : "Disabled")} cres debug."); + return; } if (!float.TryParse(arguments, CultureInfo.InvariantCulture, out float value)) diff --git a/CustomResolution/Configuration.cs b/CustomResolution/Configuration.cs index c428bae..e7a14fb 100644 --- a/CustomResolution/Configuration.cs +++ b/CustomResolution/Configuration.cs @@ -15,7 +15,7 @@ public class Configuration : IPluginConfiguration public uint Width = 1024; public uint Height = 1024; - public bool IsDXVKDWMHackEnabled = false; + public DXVKDWMHackMode DXVKDWMHackMode = DXVKDWMHackMode.Off; [NonSerialized] private DalamudPluginInterface? pluginInterface; @@ -30,3 +30,29 @@ public class Configuration : IPluginConfiguration pluginInterface!.SavePluginConfig(this); } } + +// Must explicitly map to integer values, blame Newtonsoft.JSON +public enum DXVKDWMHackMode +{ + Off = 0, + UnsetPopup = 1, + SetClientEdge = 2 +} + +public static class DXVKDWMHackModeExt +{ + public static string ToHumanNameString(this DXVKDWMHackMode mode) => mode switch + { + DXVKDWMHackMode.Off => "Off", + DXVKDWMHackMode.UnsetPopup => "Best case: Disable WS_POPUP", + DXVKDWMHackMode.SetClientEdge => "Worst case: Enable WS_EX_CLIENTEDGE", + _ => mode.ToString(), + }; + + public static string? ToHumanInfoString(this DXVKDWMHackMode mode) => mode switch + { + DXVKDWMHackMode.UnsetPopup => "Works best with NVIDIA GPUs.", + DXVKDWMHackMode.SetClientEdge => "Adds a 2 pixel border around the game.", + _ => null + }; +} diff --git a/CustomResolution/CustomResolution.csproj b/CustomResolution/CustomResolution.csproj index 21b3d4c..c407a18 100644 --- a/CustomResolution/CustomResolution.csproj +++ b/CustomResolution/CustomResolution.csproj @@ -3,7 +3,7 @@ 0x0ade - 0.1.0.5 + 0.2.0.0 diff --git a/CustomResolution/Hooks/WndProcHook.cs b/CustomResolution/Hooks/WndProcHook.cs index 4a7dcfc..b987144 100644 --- a/CustomResolution/Hooks/WndProcHook.cs +++ b/CustomResolution/Hooks/WndProcHook.cs @@ -4,16 +4,14 @@ using System; using System.Linq; using System.Reflection.Emit; using TerraFX.Interop.Windows; +using static TerraFX.Interop.Windows.Windows; namespace CustomResolution.Hooks; // THIS. IS. UGLY. -public sealed class WndProcHook : IDisposable +public sealed unsafe class WndProcHook : IDisposable { - private const uint WM_MOUSEFIRST = 0x0200; - private const uint WM_MOUSELAST = 0x0209; - private static WndProcHook? _instance; private readonly WndProcHookManager _manager; @@ -75,24 +73,45 @@ public sealed class WndProcHook : IDisposable private void Invoke(WndProcEventArgs args) { - if (!(WM_MOUSEFIRST <= args.Message && args.Message <= WM_MOUSELAST)) + if (Service.Plugin is not { } plugin) { return; } - ParamToCoords(args.LParam, out int x, out int y); + if (WM.WM_MOUSEFIRST <= args.Message && args.Message <= WM.WM_MOUSELAST) + { + ParamToCoords(args.LParam, out int x, out int y); #if false - Service.PluginLog.Debug($"WM_MOUSE A @ {x} {y}"); + Service.PluginLog.Debug($"WM_MOUSE A @ {x} {y}"); #endif - Service.Plugin.ConvertCoordsWinToGame(ref x, ref y); + plugin.ConvertCoordsWinToGame(ref x, ref y); #if false - Service.PluginLog.Debug($"WM_MOUSE B @ {x} {y}"); + Service.PluginLog.Debug($"WM_MOUSE B @ {x} {y}"); #endif - args.LParam = CoordsToParam(x, y); + args.LParam = CoordsToParam(x, y); + } + + if (args.Message == WM.WM_NCCALCSIZE && args.WParam != 0 && plugin.CurrentBorderlessFullscreen && + Service.Config.DXVKDWMHackMode >= DXVKDWMHackMode.SetClientEdge) + { + NCCALCSIZE_PARAMS* ncsize = (NCCALCSIZE_PARAMS*) args.LParam; + MONITORINFO monitorInfo = new() + { + cbSize = (uint) sizeof(MONITORINFO) + }; + + if (MonitorFromWindow(plugin.CurrentHWND, MONITOR.MONITOR_DEFAULTTONEAREST) is { } monitor && monitor != HMONITOR.NULL && + GetMonitorInfo(monitor, &monitorInfo)) + { + ncsize->rgrc[0] = monitorInfo.rcMonitor; + + args.SuppressCall = true; + } + } } private void Apply() diff --git a/CustomResolution/Plugin.cs b/CustomResolution/Plugin.cs index 190dd15..7bc7a9d 100644 --- a/CustomResolution/Plugin.cs +++ b/CustomResolution/Plugin.cs @@ -22,7 +22,7 @@ public sealed unsafe class Plugin : IDalamudPlugin private HWND _currentHwnd; private RECT _currentClientRect; private RECT _currentWindowRect; - private bool _currentDXVKDWMHack = false; + private DXVKDWMHackMode _currentDXVKDWMHackMode = DXVKDWMHackMode.Off; public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { @@ -55,11 +55,17 @@ public sealed unsafe class Plugin : IDalamudPlugin public string Name => "CustomResolution"; + public HWND CurrentHWND { get; private set; } + 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 IsDebug { get; set; } + public void Dispose() { _tickCount = 0; @@ -204,10 +210,43 @@ public sealed unsafe class Plugin : IDalamudPlugin dev->RequestResolutionChange = 1; } - if (Service.Config.IsDXVKDWMHackEnabled && !_unloading) + // TODO: This isn't accurate! Figure out how to read the game's settings instead. + CurrentBorderlessFullscreen = (GetWindowLong(_currentHwnd, GWL.GWL_STYLE) & WS.WS_SYSMENU) == 0; + + if (Service.Config.DXVKDWMHackMode != DXVKDWMHackMode.Off && !_unloading) { -#if false - Service.PluginLog.Info($"STYLE: 0x{GetWindowLong(_currentHwnd, GWL.GWL_STYLE):X8}"); + 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 + */ + uint styleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_STYLE); + uint exstyleOrig = (uint) GetWindowLong(_currentHwnd, GWL.GWL_EXSTYLE); + + uint style = styleOrig; + uint exstyle = exstyleOrig; + + bool fullscreen = (style & WS.WS_SYSMENU) == 0; + + if (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]; @@ -217,41 +256,11 @@ public sealed unsafe class Plugin : IDalamudPlugin Service.PluginLog.Info($"CLASS: {new string((char*) name.GetPointer(0))}"); Service.PluginLog.Info($"CLASS.style: 0x{wce.style:X8}"); -#endif - - SetDXVKDWMHack(true); } - else if (!Service.Config.IsDXVKDWMHackEnabled && _currentDXVKDWMHack) - { - SetDXVKDWMHack(false); - } - - _currentDXVKDWMHack = Service.Config.IsDXVKDWMHackEnabled; - - CurrentWidth = width; - CurrentHeight = height; - CurrentWindowWidth = (uint) rectWidth; - CurrentWindowHeight = (uint) rectHeight; - } - - private void SetDXVKDWMHack(bool enabled) - { - /* 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 - */ - - uint style = (uint) GetWindowLong(_currentHwnd, GWL.GWL_STYLE); - bool fullscreen = (style & WS.WS_SYSMENU) == 0; - - /* Alternative hacks: - * - WS_EX_CLIENTEDGE, at the cost of having a 2px border on each side. - */ if (fullscreen) { - if (enabled) + if (mode >= DXVKDWMHackMode.UnsetPopup) { style &= ~WS.WS_POPUP; } @@ -261,7 +270,42 @@ public sealed unsafe class Plugin : IDalamudPlugin } } - SetWindowLong(_currentHwnd, GWL.GWL_STYLE, (int) style); + if (fullscreen && mode >= DXVKDWMHackMode.SetClientEdge) + { + exstyle |= WS.WS_EX_CLIENTEDGE; + exstyle |= WS.WS_EX_COMPOSITED; + } + else + { + exstyle &= ~(uint) WS.WS_EX_CLIENTEDGE; + exstyle &= ~(uint) WS.WS_EX_COMPOSITED; + } + + if (IsDebug) + { + Service.PluginLog.Info($"NEWSTYLE: 0x{style:X8}"); + Service.PluginLog.Info($"NEWEXSTYLE: 0x{exstyle:X8}"); + } + + if (style != styleOrig || exstyle != exstyleOrig) + { + if (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 (IsDebug) + { + Service.PluginLog.Info("SAME"); + } + + _currentDXVKDWMHackMode = mode; } private void OnFrameworkUpdate(IFramework framework) diff --git a/CustomResolution/Windows/ConfigWindow.cs b/CustomResolution/Windows/ConfigWindow.cs index 24333bb..bedccc3 100644 --- a/CustomResolution/Windows/ConfigWindow.cs +++ b/CustomResolution/Windows/ConfigWindow.cs @@ -14,16 +14,13 @@ public class ConfigWindow : Window, IDisposable private bool _configIsScale; private float _configScale; private int[] _configWH = new int[2]; - private bool _configIsDXVKDWMHackEnabled; + private DXVKDWMHackMode _configDXVKDWMHackMode; public ConfigWindow() : base( "CustomResolution", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | - ImGuiWindowFlags.NoScrollWithMouse) + ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.AlwaysAutoResize) { - Size = new Vector2(430, 212); - SizeCondition = ImGuiCond.Always; - UpdateFromConfig(); } @@ -39,7 +36,7 @@ public class ConfigWindow : Window, IDisposable _configScale = config.Scale; _configWH[0] = (int) config.Width; _configWH[1] = (int) config.Height; - _configIsDXVKDWMHackEnabled = config.IsDXVKDWMHackEnabled; + _configDXVKDWMHackMode = config.DXVKDWMHackMode; } public void UpdateToConfig() @@ -51,7 +48,7 @@ public class ConfigWindow : Window, IDisposable config.Scale = _configScale; config.Width = (uint) _configWH[0]; config.Height = (uint) _configWH[1]; - config.IsDXVKDWMHackEnabled = _configIsDXVKDWMHackEnabled; + config.DXVKDWMHackMode = _configDXVKDWMHackMode; config.Save(); } @@ -97,13 +94,31 @@ public class ConfigWindow : Window, IDisposable ImGui.EndDisabled(); } - ImGui.Checkbox("Apply borderless window workaround", ref _configIsDXVKDWMHackEnabled); + if (ImGui.BeginCombo("Borderless window workaround", _configDXVKDWMHackMode.ToHumanNameString())) + { + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToHumanNameString(), _configDXVKDWMHackMode == mode, ImGuiSelectableFlags.None)) + { + _configDXVKDWMHackMode = mode; + } + + if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info) + { + ImGui.SetTooltip(info); + } + } + + ImGui.EndCombo(); + } + if (ImGui.IsItemHovered()) { - ImGui.SetTooltip(@"Fixes DXVK borderless window acting like exclusive fullscreen. -In other words: Fixes black screen flashing when alt-tabbing. + ImGui.SetTooltip(@"Fixes DXVK borderless window causing black screens when alt-tabbing. This can *possibly* impact performance, depending on your Windows version and GPU. + Feel free to experiment with this toggle. +Make sure to switch the game to windowed and then back to borderless windowed when changing. Works even with the scaling above disabled.