Skip to content

Commit

Permalink
Merge pull request #7 from chickensoft-games/feat/native-resolution
Browse files Browse the repository at this point in the history
feat: determine correct scale factor and native pixel resolution on windows
  • Loading branch information
jolexxa authored Feb 11, 2025
2 parents 1bdbbd3 + 2d67503 commit d50561f
Show file tree
Hide file tree
Showing 19 changed files with 513 additions and 15 deletions.
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));

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.SizeOf(System.Type)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

Using member 'System.Runtime.InteropServices.Marshal.SizeOf(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Marshalling code for the object might not be available. Use the SizeOf<T> overload instead.

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.SizeOf(System.Type)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

Using member 'System.Runtime.InteropServices.Marshal.SizeOf(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Marshalling code for the object might not be available. Use the SizeOf<T> overload instead.

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-windows

'System.Runtime.InteropServices.Marshal.SizeOf(System.Type)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-windows

Using member 'System.Runtime.InteropServices.Marshal.SizeOf(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Marshalling code for the object might not be available. Use the SizeOf<T> overload instead.

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.SizeOf(System.Type)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

Using member 'System.Runtime.InteropServices.Marshal.SizeOf(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Marshalling code for the object might not be available. Use the SizeOf<T> overload instead.

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.SizeOf(System.Type)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 55 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

Using member 'System.Runtime.InteropServices.Marshal.SizeOf(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Marshalling code for the object might not be available. Use the SizeOf<T> overload instead.
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);

Check warning on line 70 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.PtrToStructure<Chickensoft.Platform.Windows.User32.MonitorInfoEx>(nint)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 70 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.PtrToStructure<Chickensoft.Platform.Windows.User32.MonitorInfoEx>(nint)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 70 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-windows

'System.Runtime.InteropServices.Marshal.PtrToStructure<Chickensoft.Platform.Windows.User32.MonitorInfoEx>(nint)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 70 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.PtrToStructure<Chickensoft.Platform.Windows.User32.MonitorInfoEx>(nint)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 70 in Chickensoft.PlatformExt/src/windows/Monitors.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.PtrToStructure<Chickensoft.Platform.Windows.User32.MonitorInfoEx>(nint)' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

// 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>();

Check warning on line 23 in Chickensoft.PlatformExt/src/windows/lib/User32.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.SizeOf<Chickensoft.Platform.Windows.User32.MonitorInfoEx>()' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 23 in Chickensoft.PlatformExt/src/windows/lib/User32.cs

View workflow job for this annotation

GitHub Actions / build-macos

'System.Runtime.InteropServices.Marshal.SizeOf<Chickensoft.Platform.Windows.User32.MonitorInfoEx>()' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 23 in Chickensoft.PlatformExt/src/windows/lib/User32.cs

View workflow job for this annotation

GitHub Actions / build-windows

'System.Runtime.InteropServices.Marshal.SizeOf<Chickensoft.Platform.Windows.User32.MonitorInfoEx>()' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 23 in Chickensoft.PlatformExt/src/windows/lib/User32.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.SizeOf<Chickensoft.Platform.Windows.User32.MonitorInfoEx>()' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)

Check warning on line 23 in Chickensoft.PlatformExt/src/windows/lib/User32.cs

View workflow job for this annotation

GitHub Actions / build-linux

'System.Runtime.InteropServices.Marshal.SizeOf<Chickensoft.Platform.Windows.User32.MonitorInfoEx>()' uses runtime marshalling even when 'DisableRuntimeMarshallingAttribute' is applied. Use features like 'sizeof' and pointers directly to ensure accurate results. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421)
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

0 comments on commit d50561f

Please sign in to comment.