Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: determine correct scale factor and native pixel resolution on windows #7

Merged
merged 10 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Chickensoft.PlatformExt/src/extensions/Displays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ public float GetDisplayScaleFactor(Window window) {
return DisplayServer.Singleton.ScreenGetScale(window.CurrentScreen);
}

/// <summary>
/// Finds the native resolution of the screen that the given window is on using
/// platform-specific API's on macOS and Windows.
/// </summary>
/// <param name="window">Godot window.</param>
/// <returns>Native resolution on macOS or Windows.</returns>
#if GDEXTENSION
[BindMethod]
#endif
public Vector2I GetNativeResolution(Window window) {
var id = window.GetWindowId();

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
return MacOS.Displays.GetScreenResolution(
MacOS.Displays.GetCGDirectDisplayID(id)
);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
return Windows.Monitors.GetMonitorResolution(
Windows.Monitors.GetMonitorHandle(id)
);
}

return DisplayServer.Singleton.ScreenGetSize(window.CurrentScreen);
}

private static float GetDisplayScaleFactorMacOS(Window window) {
// This will always be 1, 2, or 3, due to limited information from macOS.
// This scale factor represents the type of retina display, but not the
Expand All @@ -54,6 +80,9 @@ private static float GetDisplayScaleFactorMacOS(Window window) {

var logicalResolutionRetina =
DisplayServer.Singleton.ScreenGetSize(window.CurrentScreen);

// Note that we divide by Godot's reported retina scale factor, since
// the Godot DisplayServer has scaled the logical resolution by it.
var logicalResolution = new Vector2I(
Mathf.RoundToInt(logicalResolutionRetina.X / retinaScale),
Mathf.RoundToInt(logicalResolutionRetina.Y / retinaScale)
Expand All @@ -66,6 +95,7 @@ private static float GetDisplayScaleFactorMacOS(Window window) {

private static float GetDisplayScaleFactorWindows(Window window) {
var hMonitor = Windows.Monitors.GetMonitorHandle(window.GetWindowId());

return Windows.Monitors.GetMonitorScale(hMonitor);
}
}
4 changes: 2 additions & 2 deletions Chickensoft.PlatformExt/src/macOS/Displays.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Chickensoft.Platform.MacOS;

using System;
using Godot;
using ObjC = Lib.ObjectiveC;
using CG = Lib.CoreGraphics;
using System;
using ObjC = Lib.ObjectiveC;

internal static partial class Displays {
/// <summary>
Expand Down
76 changes: 70 additions & 6 deletions Chickensoft.PlatformExt/src/windows/Monitors.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace Chickensoft.Platform.Windows;

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Godot;

internal sealed class Monitors {
Expand All @@ -9,15 +11,16 @@ internal sealed class Monitors {
/// </summary>
/// <param name="godotWindowId">Godot window id.</param>
/// <returns>Win32 monitor handle.</returns>
public static uint GetMonitorHandle(int godotWindowId) {
public static long GetMonitorHandle(int godotWindowId) {
var nativeHandle = DisplayServer
.Singleton
.WindowGetNativeHandle(DisplayServer.HandleType.WindowHandle, godotWindowId);
.WindowGetNativeHandle(
DisplayServer.HandleType.WindowHandle, godotWindowId
);

var hWnd = new IntPtr(nativeHandle);
return (uint)User32
.MonitorFromWindow(hWnd, User32.MONITOR_DEFAULTTONEAREST)
.ToInt32();
return User32
.MonitorFromWindow(hWnd, User32.MONITOR_DEFAULTTONEAREST).ToInt64();
}

/// <summary>
Expand All @@ -26,15 +29,76 @@ public static uint GetMonitorHandle(int godotWindowId) {
/// </summary>
/// <param name="hMonitor">Win32 monitor handle.</param>
/// <returns>Monitor scale factor.</returns>
public static float GetMonitorScale(uint hMonitor) {
public static float GetMonitorScale(long hMonitor) {
// We need to get the DPI of the monitor itself, not the system DPI.
// Windows 10+ only.
var oldDpiAwareness = User32.SetThreadDpiAwarenessContext(
User32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
);

Shcore.GetDpiForMonitor(
new IntPtr(hMonitor),
Shcore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
out var dpiX,
out var dpiY
);

// Restore previous thread dpi awareness context, just to be safe.
User32.SetThreadDpiAwarenessContext(oldDpiAwareness);

// https://stackoverflow.com/a/69573593
return dpiY / 96f;
}

public static Vector2I GetMonitorResolution(long hMonitor) {
var hMonitorPtr = new IntPtr(hMonitor);
var monSize = Marshal.SizeOf(typeof(User32.MonitorInfoEx));
var pMonitorInfo = Marshal.AllocHGlobal(monSize);

// We have to set the structure size first.
Marshal.WriteInt32(pMonitorInfo, monSize);

if (!User32.GetMonitorInfo(hMonitorPtr, pMonitorInfo)) {
Debug.WriteLine(
"Failed to get monitor info. Error Code: " +
Marshal.GetLastWin32Error()
);
return Vector2I.Zero;
}

var monitorInfo =
Marshal.PtrToStructure<User32.MonitorInfoEx>(pMonitorInfo);

// Create a string from the szDevice char array. Why Windows wants to use
// a string as a monitor identifier, I have no idea...

var deviceChars = monitorInfo!.szDevice;
var length = Array.IndexOf(deviceChars, '\0'); // stop at null terminator
if (length < 0) {
length = deviceChars.Length;
}

var deviceName = new string(deviceChars, 0, length);
Debug.WriteLine($"Monitor Device Name: {deviceName}");

// Create a device context for the monitor so we can use GDI api's.
var hdc = Gdi32.CreateDC(null!, deviceName, null!, IntPtr.Zero);
if (hdc == IntPtr.Zero) {
Debug.WriteLine(
"Failed to create device context. Error Code: " +
Marshal.GetLastWin32Error()
);
return Vector2I.Zero;
}

try {
// Actually get the monitor's native resolution :P
var w = Gdi32.GetDeviceCaps(hdc, Gdi32.DESKTOP_HORZ_RES);
var h = Gdi32.GetDeviceCaps(hdc, Gdi32.DESKTOP_VERT_RES);
return new Vector2I(w, h).Abs();
}
finally {
Gdi32.DeleteDC(hdc);
}
}
}
30 changes: 30 additions & 0 deletions Chickensoft.PlatformExt/src/windows/lib/Gdi32.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Chickensoft.Platform.Windows;

using System;
using System.Runtime.InteropServices;

internal static partial class Gdi32 {
public const string GDI32 = "gdi32.dll";
public const int DESKTOP_VERT_RES = 117;
public const int DESKTOP_HORZ_RES = 118;

[LibraryImport(
GDI32,
SetLastError = true,
StringMarshalling = StringMarshalling.Utf16,
EntryPoint = "CreateDCW"
)]
internal static partial IntPtr CreateDC(
string pwszDriver, // typically null for display
string pwszDevice, // "\\.\DISPLAYx"
string pwszOutput, // null
IntPtr lpInitData // IntPtr.Zero
);

[LibraryImport(GDI32, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DeleteDC(IntPtr hdc);

[LibraryImport(GDI32, SetLastError = true)]
internal static partial int GetDeviceCaps(IntPtr hdc, int nIndex);
}
13 changes: 9 additions & 4 deletions Chickensoft.PlatformExt/src/windows/lib/Shcore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Chickensoft.Platform.Windows;


internal static partial class Shcore {
public const string SHCORE = "shcore.dll";
public enum DEVICE_SCALE_FACTOR {
DEVICE_SCALE_FACTOR_INVALID = 0,
SCALE_100_PERCENT = 100,
Expand All @@ -31,9 +32,13 @@ public enum MONITOR_DPI_TYPE {
MDT_RAW_DPI = 2
}

[LibraryImport("shcore.dll", SetLastError = true)]
internal static partial int GetScaleFactorForMonitor(IntPtr hMonitor, out DEVICE_SCALE_FACTOR pScale);
[LibraryImport(SHCORE, SetLastError = true)]
internal static partial int GetScaleFactorForMonitor(
IntPtr hMonitor, out DEVICE_SCALE_FACTOR pScale
);

[LibraryImport("shcore.dll", SetLastError = true)]
internal static partial int GetDpiForMonitor(IntPtr hMonitor, MONITOR_DPI_TYPE dpiType, out uint dpiX, out uint dpiY);
[LibraryImport(SHCORE, SetLastError = true)]
internal static partial int GetDpiForMonitor(
IntPtr hMonitor, MONITOR_DPI_TYPE dpiType, out uint dpiX, out uint dpiY
);
}
54 changes: 53 additions & 1 deletion Chickensoft.PlatformExt/src/windows/lib/User32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,62 @@ namespace Chickensoft.Platform.Windows;
using System.Runtime.InteropServices;

internal static partial class User32 {
public const string USER32 = "user32.dll";
public const int MONITOR_DEFAULTTONULL = 0x00000000;
public const int MONITOR_DEFAULTTOPRIMARY = 0x00000001;
public const int MONITOR_DEFAULTTONEAREST = 0x00000002;

[LibraryImport("user32.dll", SetLastError = true)]
#pragma warning disable IDE1006 // Naming Styles
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public sealed class MonitorInfoEx {
public int cbSize;
public Rect rcMonitor;
public Rect rcWork;
public int dwFlags;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public char[] szDevice;

public MonitorInfoEx() {
cbSize = Marshal.SizeOf<MonitorInfoEx>();
szDevice = new char[32];
}
}
#pragma warning restore IDE1006 // Naming Styles

[StructLayout(LayoutKind.Sequential)]
public struct Rect {
public int Left;
public int Top;
public int Right;
public int Bottom;

public readonly int Width => Right - Left;
public readonly int Height => Bottom - Top;
}

[LibraryImport(USER32, SetLastError = true)]
public static partial IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);

[LibraryImport(
USER32,
StringMarshalling = StringMarshalling.Utf16,
SetLastError = true,
EntryPoint = "GetMonitorInfoW"
)]
[return: MarshalAs(UnmanagedType.I1)]
public static partial bool GetMonitorInfo(IntPtr hMonitor, IntPtr lpmi);

public static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new(-1);
public static readonly IntPtr DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = new(-2);
public static readonly IntPtr DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE =
new(-3);
public static readonly IntPtr DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 =
new(-4);
public static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED =
new(-5);

[LibraryImport(USER32, SetLastError = true)]
internal static partial IntPtr SetThreadDpiAwarenessContext(
IntPtr dpiContext
);
}
4 changes: 4 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@
"fabrik",
"framebuffer",
"GDEXTENSION",
"GDISCALED",
"gdscript",
"globaltool",
"gltf",
"godotengine",
"godotenv",
"hmac",
"HORZ",
"hwnd",
"iossimulator",
"issuecomment",
Expand All @@ -64,6 +66,7 @@
"lightmapper",
"lihop",
"linecoverage",
"lpmi",
"maccatalyst",
"Mathf",
"methodcoverage",
Expand All @@ -86,6 +89,7 @@
"OPTOUT",
"paramref",
"pascalcase",
"pwsz",
"randomizer",
"raymarch",
"reloadable",
Expand Down
2 changes: 1 addition & 1 deletion sandbox/Chickensoft.Platform.Sandbox/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ config_version=5
[application]

config/name="Chickensoft.Platform.Sandbox"
run/main_scene="uid://cywpu6lxdjhuu"
run/main_scene="res://src/game/Game.tscn"
config/features=PackedStringArray("4.4")
config/icon="res://icon.png"

Expand Down
6 changes: 5 additions & 1 deletion sandbox/Chickensoft.Platform.Sandbox/src/game/game.gd
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
extends Control

func _ready() -> void:
print("ready")
var displays = Displays.new()
var scaleFactor = displays.GetDisplayScaleFactor(get_window())
var window := get_window()
var scaleFactor = displays.GetDisplayScaleFactor(window)
print("scale factor: ", scaleFactor)
var resolution = displays.GetNativeResolution(window)
print("native resolution: ", resolution)
15 changes: 15 additions & 0 deletions sandbox/Chickensoft.Platform.SandboxRef/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"configurations": [
{
"name": "🕹 Debug Game",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${env:GODOT}",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal"
},
]
}
Loading