core (0.4): Implement separate display vs game scaling (thanks, NotNite!)

This commit is contained in:
Jade Macho 2025-06-29 16:52:20 +02:00
parent 78ff318397
commit ff12d438c6
Signed by: 0x0ade
GPG key ID: E1960710FE4FBEEF
16 changed files with 1126 additions and 597 deletions

5
.editorconfig Normal file
View file

@ -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

View file

@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResolution2782", "Cus
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ECEDCE5-D60F-4A8A-AB33-4131F5C7371C}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ECEDCE5-D60F-4A8A-AB33-4131F5C7371C}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
EndProjectSection EndProjectSection
EndProject EndProject

View file

@ -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.");
}
}

View file

@ -7,12 +7,12 @@ public sealed class MainCmd : Cmd
{ {
public override string Name => "cres"; 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" + $"\tExamples:\n" +
$"\tTo open the settings:\n\t\t{FullName}\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 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 display scale:\n\t\t{FullName} 1.5\n" +
$"\tTo set the resolution:\n\t\t{FullName} 1920 1080"; $"\tTo set the display resolution:\n\t\t{FullName} 1920 1080";
public override void Run(string arguments) public override void Run(string arguments)
{ {
@ -34,33 +34,33 @@ public sealed class MainCmd : Cmd
return; return;
} }
Service.Config.IsScale = false; Service.Config._.Display.IsScale = false;
Service.Config.Width = width; Service.Config._.Display.Width = width;
Service.Config.Height = height; Service.Config._.Display.Height = height;
Service.Config.Save(); Service.Config.Save();
Service.PrintChat("Updated custom resolution."); Service.PrintChat("Updated custom display resolution.");
return; return;
} }
switch (arguments.ToLowerInvariant()) switch (arguments.ToLowerInvariant())
{ {
case "on": case "on":
Service.Config.IsEnabled = true; Service.Config._.Display.IsEnabled = true;
Service.Config.Save(); Service.Config.Save();
Service.PrintChat("Enabled custom resolution."); Service.PrintChat("Enabled custom display resolution.");
return; return;
case "off": case "off":
Service.Config.IsEnabled = false; Service.Config._.Display.IsEnabled = false;
Service.Config.Save(); Service.Config.Save();
Service.PrintChat("Disabled custom resolution."); Service.PrintChat("Disabled custom display resolution.");
return; return;
case "toggle": case "toggle":
Service.Config.IsEnabled = !Service.Config.IsEnabled; Service.Config._.Display.IsEnabled = !Service.Config._.Display.IsEnabled;
Service.Config.Save(); Service.Config.Save();
Service.PrintChat($"{(Service.Config.IsEnabled ? "Enabled" : "Disabled")} custom resolution."); Service.PrintChat($"{(Service.Config._.Display.IsEnabled ? "Enabled" : "Disabled")} custom display resolution.");
return; return;
case "debugon": case "debugon":
@ -95,10 +95,10 @@ public sealed class MainCmd : Cmd
return; return;
} }
Service.Config.IsScale = true; Service.Config._.Display.IsScale = true;
Service.Config.Scale = value; Service.Config._.Display.Scale = value;
Service.Config.Save(); Service.Config.Save();
Service.PrintChat("Updated custom resolution scale."); Service.PrintChat("Updated custom display resolution scale.");
} }
} }

View file

@ -1,151 +1,77 @@
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Plugin; using Dalamud.Plugin;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic;
using System.Reflection;
namespace CustomResolution; namespace CustomResolution;
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration 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; public bool IsEnabled = true;
[Obsolete]
public bool IsScale = true; public bool IsScale = true;
[Obsolete]
public float Scale = 1f; public float Scale = 1f;
[Obsolete]
public uint Width = 1024; public uint Width = 1024;
[Obsolete]
public uint Height = 1024; public uint Height = 1024;
[Obsolete]
public VirtualKey HotkeyKey = VirtualKey.NO_KEY; public VirtualKey HotkeyKey = VirtualKey.NO_KEY;
[Obsolete]
public ModifierKey HotkeyModifier = ModifierKey.NONE; public ModifierKey HotkeyModifier = ModifierKey.NONE;
[Obsolete]
public DXVKDWMHackMode DXVKDWMHackMode = DXVKDWMHackMode.Off; public DXVKDWMHackMode DXVKDWMHackMode = DXVKDWMHackMode.Off;
[Obsolete]
public MinSizeMode MinSizeMode = MinSizeMode.Unchanged; public MinSizeMode MinSizeMode = MinSizeMode.Unchanged;
[NonSerialized] [NonSerialized]
private IDalamudPluginInterface? pluginInterface; private IDalamudPluginInterface pluginInterface = null!;
internal void Initialize(IDalamudPluginInterface pluginInterface) internal void Initialize(IDalamudPluginInterface pluginInterface)
{ {
this.pluginInterface = pluginInterface; this.pluginInterface = pluginInterface;
Upgrade();
} }
public void Save() public void Save()
{ {
pluginInterface!.SavePluginConfig(this); _.Display.Clamp();
_.Game.Clamp();
pluginInterface.SavePluginConfig(this);
Service.PluginUI.UpdateFromConfig(); Service.PluginUI.UpdateFromConfig();
} }
}
// Must explicitly map to integer values, blame Newtonsoft.JSON private void Upgrade()
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", #pragma warning disable CS0612 // Type or member is obsolete, but we want to migrate from it.
DXVKDWMHackMode.UnsetPopup => "Best: -WS_POPUP", #pragma warning disable CS0618 // Type or member is obsolete, but we want to migrate from it.
DXVKDWMHackMode.SetClientEdgeResize => "+WS_EX_CLIENTEDGE (resize)", if (Version == 0)
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<VirtualKey, string> _strings = new();
static VirtualKeyExt()
{
foreach (var field in typeof(VirtualKey).GetFields(BindingFlags.Public | BindingFlags.Static))
{ {
var key = (VirtualKey) field.GetValue(null)!; Service.PluginLog.Info("Migrating config V0 to V1");
_strings[key] = key.GetFancyName(); 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
};
} }

View file

@ -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<VirtualKey, string> _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;
}

View file

@ -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;
}
}
}
}

View file

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>0x0ade</Authors> <Authors>0x0ade</Authors>
<Company></Company> <Company></Company>
<Version>0.3.2.0</Version> <Version>0.4.0.0</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl></PackageProjectUrl> <PackageProjectUrl></PackageProjectUrl>

View file

@ -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<ushort> 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,
/// <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
}

View file

@ -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
}
}

View file

@ -80,7 +80,7 @@ public sealed unsafe class CursorPosHooks : IDisposable
Service.PluginLog.Debug($"GetCursorPos A @ {lpPoint->x} {lpPoint -> y}"); Service.PluginLog.Debug($"GetCursorPos A @ {lpPoint->x} {lpPoint -> y}");
#endif #endif
Service.Plugin.ConvertCoordsGlobalToGame(ref lpPoint->x, ref lpPoint->y); Service.DisplaySize.ConvertCoordsGlobalToGame(ref lpPoint->x, ref lpPoint->y);
#if false #if false
Service.PluginLog.Debug($"GetCursorPos B @ {lpPoint->x} {lpPoint->y}"); 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}"); Service.PluginLog.Debug($"SetCursorPos A @ {x} {y}");
#endif #endif
Service.Plugin.ConvertCoordsGameToGlobal(ref x, ref y); Service.DisplaySize.ConvertCoordsGameToGlobal(ref x, ref y);
#if false #if false
Service.PluginLog.Debug($"SetCursorPos B @ {x} {y}"); Service.PluginLog.Debug($"SetCursorPos B @ {x} {y}");

View file

@ -50,9 +50,9 @@ public sealed unsafe class WindowRectHooks : IDisposable
Service.PluginLog.Debug($"GetClientRectDetour A @ {lpRect->left} {lpRect->top} {lpRect->right} {lpRect->bottom}"); Service.PluginLog.Debug($"GetClientRectDetour A @ {lpRect->left} {lpRect->top} {lpRect->right} {lpRect->bottom}");
#endif #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 #if false
@ -73,9 +73,9 @@ public sealed unsafe class WindowRectHooks : IDisposable
Service.PluginLog.Debug($"SetWindowPosDetour A @ {X} {Y} {cx} {cy}"); Service.PluginLog.Debug($"SetWindowPosDetour A @ {X} {Y} {cx} {cy}");
#endif #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 #if false

View file

@ -112,7 +112,7 @@ public sealed unsafe class WndProcHook : IDisposable
} }
if (args.Message == WM.WM_WINDOWPOSCHANGING && if (args.Message == WM.WM_WINDOWPOSCHANGING &&
Service.Config.MinSizeMode == MinSizeMode.Unlocked) Service.Config._.MinSizeMode == MinSizeMode.Unlocked)
{ {
args.SuppressCall = true; args.SuppressCall = true;
} }
@ -127,8 +127,8 @@ public sealed unsafe class WndProcHook : IDisposable
{ {
ParamToCoords(args.LParam, out int x, out int y); ParamToCoords(args.LParam, out int x, out int y);
Service.PluginLog.Debug($"WM_SIZE {x} x {y}"); Service.PluginLog.Debug($"WM_SIZE {x} x {y}");
Service.Plugin.Update(); Service.DisplaySize.Update();
args.LParam = SizeToParam(Service.Plugin.CurrentWidth, Service.Plugin.CurrentHeight); args.LParam = SizeToParam(Service.DisplaySize.CurrentWidth, Service.DisplaySize.CurrentHeight);
} }
if (WM.WM_MOUSEFIRST <= args.Message && args.Message <= WM.WM_MOUSELAST) 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}"); Service.PluginLog.Debug($"WM_MOUSE A @ {x} {y}");
#endif #endif
plugin.ConvertCoordsWinToGame(ref x, ref y); Service.DisplaySize.ConvertCoordsWinToGame(ref x, ref y);
#if false #if false
Service.PluginLog.Debug($"WM_MOUSE B @ {x} {y}"); Service.PluginLog.Debug($"WM_MOUSE B @ {x} {y}");
@ -148,8 +148,8 @@ public sealed unsafe class WndProcHook : IDisposable
args.LParam = CoordsToParam(x, y); args.LParam = CoordsToParam(x, y);
} }
if (args.Message == WM.WM_NCCALCSIZE && args.WParam != 0 && plugin.CurrentBorderlessFullscreen && if (args.Message == WM.WM_NCCALCSIZE && args.WParam != 0 && Service.DisplaySize.CurrentBorderlessFullscreen &&
Service.Config.DXVKDWMHackMode.IsSetClientEdge()) Service.Config._.DXVKDWMHackMode.IsSetClientEdge())
{ {
var ncsize = (NCCALCSIZE_PARAMS*) args.LParam; var ncsize = (NCCALCSIZE_PARAMS*) args.LParam;
MONITORINFO monitorInfo = new() MONITORINFO monitorInfo = new()
@ -157,12 +157,12 @@ public sealed unsafe class WndProcHook : IDisposable
cbSize = (uint) sizeof(MONITORINFO) 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)) GetMonitorInfo(monitor, &monitorInfo))
{ {
ncsize->rgrc[0] = monitorInfo.rcMonitor; ncsize->rgrc[0] = monitorInfo.rcMonitor;
switch (Service.Config.DXVKDWMHackMode) switch (Service.Config._.DXVKDWMHackMode)
{ {
case DXVKDWMHackMode.SetClientEdgeResize: case DXVKDWMHackMode.SetClientEdgeResize:
ncsize->rgrc[0].bottom += 1; ncsize->rgrc[0].bottom += 1;

View file

@ -4,6 +4,7 @@ using Dalamud.Game.Config;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.Interop; using FFXIVClientStructs.Interop;
using ImGuiNET; using ImGuiNET;
@ -11,6 +12,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Xml.Linq;
using TerraFX.Interop.Windows; using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows; using static TerraFX.Interop.Windows.Windows;
@ -20,28 +22,18 @@ public sealed unsafe class Plugin : IDalamudPlugin
{ {
private object _disposeLock = new(); private object _disposeLock = new();
private readonly HRGN _invisibleRgn;
private readonly List<Cmd> _cmds; private readonly List<Cmd> _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 _ignoreConfigChanges = false;
private bool _currentlyFakeResize = false;
private bool _requestedResolutionChange = false;
public Plugin(IDalamudPluginInterface pluginInterface) public Plugin(IDalamudPluginInterface pluginInterface)
{ {
_invisibleRgn = CreateRectRgn(0, 0, -1, -1);
pluginInterface.Create<Service>(); pluginInterface.Create<Service>();
Service.Plugin = this; 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 = Service.PluginInterface.GetPluginConfig() as Configuration ?? new();
Service.Config.Initialize(Service.PluginInterface); Service.Config.Initialize(Service.PluginInterface);
@ -68,25 +60,18 @@ public sealed unsafe class Plugin : IDalamudPlugin
public string Name => "CustomResolution"; public string Name => "CustomResolution";
public HWND CurrentHWND => _currentHwnd; public bool Unloading { get; private set; } = false;
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() public void Dispose()
{ {
_unloading = true; Unloading = true;
lock (_disposeLock) lock (_disposeLock)
{ {
Service.Framework.Update -= OnFrameworkUpdate; Service.Framework.Update -= OnFrameworkUpdate;
Service.GameConfig.SystemChanged -= OnSystemConfigChanged; Service.GameConfig.SystemChanged -= OnSystemConfigChanged;
Service.DisplaySize.Dispose();
Service.Framework.RunOnFrameworkThread(Update); Service.GameSize.Dispose();
} }
foreach (Cmd cmd in _cmds) foreach (Cmd cmd in _cmds)
@ -103,326 +88,23 @@ public sealed unsafe class Plugin : IDalamudPlugin
Service.Plugin = null!; Service.Plugin = null!;
} }
public void ConvertCoordsWinToGame(ref int x, ref int y) private void OnFrameworkUpdateForSize(ref ConfigurationV1.SizeConfig size)
{ {
if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) var io = ImGui.GetIO();
{
return;
}
var scaleX = CurrentWidth / (float) CurrentWindowWidth; if (size.HotkeyKey != VirtualKey.NO_KEY &&
var scaleY = CurrentHeight / (float) CurrentWindowHeight; (ImGuiNative.igIsKeyPressed((ImGuiKey) size.HotkeyKey, 0) != 0 || Service.KeyState.GetRawValue(size.HotkeyKey) != 0) &&
(size.HotkeyModifier switch
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)
{ {
_currentHwndMismatch = true; ModifierKey.NONE => !io.KeyCtrl && !io.KeyAlt && !io.KeyShift,
Service.PluginLog.Error($"HWND MISMATCH between GameWindow and Device: 0x{(long) (IntPtr) (_currentHwnd):X16} vs 0x{(long) *_dev_hWnd:X16}"); ModifierKey.CTRL => io.KeyCtrl && !io.KeyAlt && !io.KeyShift,
Service.PluginLog.Info($"dev is at: 0x{(long) (IntPtr) dev:X16}"); ModifierKey.ALT => !io.KeyCtrl && io.KeyAlt && !io.KeyShift,
CheckHWND("GameWindow", _currentHwnd); ModifierKey.SHIFT => !io.KeyCtrl && !io.KeyAlt && io.KeyShift,
CheckHWND("Device", (HWND) (*_dev_hWnd)); _ => false,
} }))
return;
}
_currentHwndMismatch = false;
fixed (RECT* currentClientRectPtr = &_currentClientRect)
{ {
Service.WindowRectHooks.GetClientRectOrig(_currentHwnd, currentClientRectPtr); size.IsEnabled = !size.IsEnabled;
} Service.Config.Save();
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<ushort> 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()}");
} }
} }
@ -430,24 +112,11 @@ public sealed unsafe class Plugin : IDalamudPlugin
{ {
lock (_disposeLock) lock (_disposeLock)
{ {
var io = ImGui.GetIO(); OnFrameworkUpdateForSize(ref Service.Config._.Display);
OnFrameworkUpdateForSize(ref Service.Config._.Game);
if (Service.Config.HotkeyKey != VirtualKey.NO_KEY && Service.DisplaySize.Update();
(ImGuiNative.igIsKeyPressed((ImGuiKey) Service.Config.HotkeyKey, 0) != 0 || Service.KeyState.GetRawValue(Service.Config.HotkeyKey) != 0) && Service.GameSize.Update();
(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();
} }
} }
@ -469,11 +138,12 @@ public sealed unsafe class Plugin : IDalamudPlugin
case SystemConfigOption.ScreenHeight: case SystemConfigOption.ScreenHeight:
if (Service.DebugConfig.SetWindowSizeMode == SetWindowSizeMode.InterceptSystemConfig) if (Service.DebugConfig.SetWindowSizeMode == SetWindowSizeMode.InterceptSystemConfig)
{ {
var d = Service.DisplaySize;
var name = e.ConfigOption.ToString(); var name = e.ConfigOption.ToString();
var valueOrig = Service.GameConfig.System.GetUInt(name); var valueOrig = Service.GameConfig.System.GetUInt(name);
var isW = e.ConfigOption == SystemConfigOption.ScreenWidth; 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); var valueNew = (uint) Math.Round(valueOrig * scale);
Service.PluginLog.Info($"Intercepting config value change for {name}: {valueOrig} -> {valueNew}"); 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,
/// <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
}

View file

@ -10,6 +10,9 @@ public sealed class Service
{ {
public static Plugin Plugin { get; internal set; } = null!; 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 Configuration Config { get; internal set; } = null!;
public static DebugConfiguration DebugConfig { get; internal set; } = null!; public static DebugConfiguration DebugConfig { get; internal set; } = null!;

View file

@ -1,9 +1,12 @@
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using ImGuiNET; using ImGuiNET;
using System; using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Xml.Linq;
using TerraFX.Interop.Windows;
namespace CustomResolution.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 VirtualKey[] _validKeys = new VirtualKey[] { VirtualKey.NO_KEY }.Union(Service.KeyState.GetValidVirtualKeys()).ToArray();
private int[] _displayCurrentWH = new int[2]; private readonly int[] _displayCurrentWH = new int[2];
private int[] _displayCurrentWindowWH = new int[2]; private readonly int[] _displayCurrentWindowWH = new int[2];
private bool _configIsEnabled; private ConfigurationV1 _;
private VirtualKey _configHotkeyKey;
private ModifierKey _configHotkeyModifier;
private bool _configIsScale;
private float _configScale;
private int[] _configWH = new int[2];
private DXVKDWMHackMode _configDXVKDWMHackMode;
private MinSizeMode _configMinSizeMode;
public ConfigWindow() : base( public ConfigWindow() : base(
"CustomResolution", "CustomResolution",
@ -36,65 +32,152 @@ public class ConfigWindow : Window, IDisposable
public void UpdateFromConfig() public void UpdateFromConfig()
{ {
var config = Service.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;
} }
public void UpdateToConfig() public void UpdateToConfig()
{ {
var config = Service.Config; Service.Config._ = _;
Service.Config.Save();
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();
} }
public override void Draw() public override void Draw()
{ {
var plugin = Service.Plugin;
var save = false; var save = false;
_displayCurrentWH[0] = (int) plugin.CurrentWidth; _displayCurrentWH[0] = (int) Service.DisplaySize.CurrentWidth;
_displayCurrentWH[1] = (int) plugin.CurrentHeight; _displayCurrentWH[1] = (int) Service.DisplaySize.CurrentHeight;
_displayCurrentWindowWH[0] = (int) plugin.CurrentWindowWidth; _displayCurrentWindowWH[0] = (int) Service.DisplaySize.CurrentWindowWidth;
_displayCurrentWindowWH[1] = (int) plugin.CurrentWindowHeight; _displayCurrentWindowWH[1] = (int) Service.DisplaySize.CurrentWindowHeight;
ImGui.BeginDisabled(); ImGui.BeginDisabled();
ImGui.InputInt2("Current window size", ref _displayCurrentWindowWH[0]); 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.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<ResolutionScalingMode>())
{
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.SameLine();
ImGui.Dummy(new Vector2(20, 0)); ImGui.Dummy(new Vector2(20, 0));
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(80); ImGui.SetNextItemWidth(80);
if (ImGui.BeginCombo("##_configHotkeyModifier", _configHotkeyModifier.ToHumanNameString())) if (ImGui.BeginCombo($"##_{name}_configHotkeyModifier", size.HotkeyModifier.ToHumanNameString()))
{ {
foreach (var key in Enum.GetValues<ModifierKey>()) foreach (var key in Enum.GetValues<ModifierKey>())
{ {
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) if (ImGui.IsItemHovered() && key.ToHumanInfoString() is { } info)
@ -108,13 +191,13 @@ public class ConfigWindow : Window, IDisposable
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(160); ImGui.SetNextItemWidth(160);
if (ImGui.BeginCombo("##_configHotkeyKey", _configHotkeyKey.ToHumanNameString())) if (ImGui.BeginCombo($"##_{name}_configHotkeyKey", size.HotkeyKey.ToHumanNameString()))
{ {
foreach (var key in _validKeys) 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) if (ImGui.IsItemHovered() && key.ToHumanInfoString() is { } info)
@ -130,34 +213,27 @@ public class ConfigWindow : Window, IDisposable
ImGui.Text("Hotkey"); ImGui.Text("Hotkey");
ImGui.Checkbox("Use scale", ref _configIsScale); if (!scaleOnly)
if (_configIsScale)
{ {
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 else
{ {
ImGui.InputInt2("Size in pixels", ref _configWH[0]); unsafe
}
if (ImGui.BeginCombo("Borderless window workaround", _configDXVKDWMHackMode.ToHumanNameString()))
{
foreach (var mode in Enum.GetValues<DXVKDWMHackMode>())
{ {
if (ImGui.Selectable(mode.ToHumanNameString(), _configDXVKDWMHackMode == mode, ImGuiSelectableFlags.None)) ImGui.InputInt2("Size in pixels", ref size.WH[0]);
{
_configDXVKDWMHackMode = mode;
}
if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info)
{
ImGui.SetTooltip(info);
}
} }
ImGui.EndCombo();
} }
}
private void DrawCommonTab()
{
using var imTab = ImRaii.TabItem($"Common##CommonTab");
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
@ -172,13 +248,18 @@ Works even with the scaling above disabled.
Not intended to be used with proper fullscreen."); 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<MinSizeMode>()) return;
}
if (ImGui.BeginCombo("Borderless window workaround", _.DXVKDWMHackMode.ToHumanNameString()))
{
foreach (var mode in Enum.GetValues<DXVKDWMHackMode>())
{ {
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) if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info)
@ -190,14 +271,28 @@ Not intended to be used with proper fullscreen.");
ImGui.EndCombo(); 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<MinSizeMode>())
{
if (ImGui.Selectable(mode.ToHumanNameString(), _.MinSizeMode == mode, ImGuiSelectableFlags.None))
{
_.MinSizeMode = mode;
}
if (ImGui.IsItemHovered() && mode.ToHumanInfoString() is { } info)
{
ImGui.SetTooltip(info);
}
}
ImGui.EndCombo();
} }
} }
} }