435 lines
15 KiB
C#
435 lines
15 KiB
C#
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<ushort> 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,
|
|
|
|
/// <summary>
|
|
/// Fake a window resize event.
|
|
/// </summary>
|
|
FakeWindowResize = 0,
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<byte>(0x1).Ref;
|
|
public ref byte RequestResolutionChangeUnk2 => ref RefPtr.For(ref _.RequestResolutionChange).Offs<byte>(0x2).Ref;
|
|
public ref byte RequestResolutionChangeUnk3 => ref RefPtr.For(ref _.RequestResolutionChange).Offs<byte>(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
|
|
}
|