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 FloppyUtils; using ImGuiNET; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; namespace CustomResolution; public unsafe class DisplaySizeState : IDisposable { private StringBuilder _dbg = new(); private bool _dbgLog = false; 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 string DebugInfo => _dbg.ToString(); 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() { _dbg.Clear(); _dbgLog = false; 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) (nint) _currentHwnd:X16} vs 0x{(long) (nint) dev->hWnd:X16}"); Service.PluginLog.Info($"dev is at: 0x{(long) (nint) 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) (nint) (&dev->NewWidth):X16}"); //Service.PluginLog.Debug($"GameWindow->Width 0x{(long) (nint) (&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; if (Service.DebugConfig.IsDebug) { _dbg.AppendLine(@$"RESIZE {dev->RequestResolutionChange} {dev->RequestResolutionChangeUnk1} {dev->RequestResolutionChangeUnk2} {dev->RequestResolutionChangeUnk3}"); _dbgLog |= dev->RequestResolutionChange != 0; _dbgLog |= dev->RequestResolutionChangeUnk1 != 0; _dbgLog |= dev->RequestResolutionChangeUnk2 != 0; _dbgLog |= dev->RequestResolutionChangeUnk3 != 0; } if (_dbgLog) { Service.PluginLog.Debug(_dbg.ToString()); } } 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; _dbg.Clear(); if (Service.DebugConfig.IsDebug) { _dbg.AppendLine($"STYLE: 0x{style:X8}"); _dbg.AppendLine($"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); _dbg.AppendLine($"CLASS: {new string((char*) name.GetPointer(0))}"); _dbg.AppendLine($"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) { _dbg.AppendLine($"NEWSTYLE: 0x{style:X8}"); _dbg.AppendLine($"NEWEXSTYLE: 0x{exstyle:X8}"); } if (style != styleOrig || exstyle != exstyleOrig || _currentDXVKDWMHackMode != mode) { if (Service.DebugConfig.IsDebug) { _dbg.AppendLine("UPDATE"); _dbgLog = true; } 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) { _dbg.AppendLine("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()}"); } } } 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 } [StructLayout(LayoutKind.Explicit)] public unsafe struct DeviceEx { [FieldOffset(0)] public Device _; public ref byte RequestResolutionChange => ref _.RequestResolutionChange; public ref byte RequestResolutionChangeUnk1 => ref RefPtr.For(ref _.RequestResolutionChange).Offs(0x1).Ref; public ref byte RequestResolutionChangeUnk2 => ref RefPtr.For(ref _.RequestResolutionChange).Offs(0x2).Ref; public ref byte RequestResolutionChangeUnk3 => ref RefPtr.For(ref _.RequestResolutionChange).Offs(0x3).Ref; 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 }