diff --git a/sources/engine/Stride.Games/Desktop/GameWindowWinforms.cs b/sources/engine/Stride.Games/Desktop/GameWindowWinforms.cs index 439125ae9e..503a4e341b 100644 --- a/sources/engine/Stride.Games/Desktop/GameWindowWinforms.cs +++ b/sources/engine/Stride.Games/Desktop/GameWindowWinforms.cs @@ -1,6 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -#if (STRIDE_GRAPHICS_API_DIRECT3D || STRIDE_GRAPHICS_API_VULKAN) && (STRIDE_UI_WINFORMS || STRIDE_UI_WPF) +#if STRIDE_PLATFORM_WINDOWS && (STRIDE_UI_WINFORMS || STRIDE_UI_WPF) using System; using System.Diagnostics; using System.Drawing; @@ -159,6 +159,7 @@ protected override void Initialize(GameContext gameContext) gameForm.FullscreenToggle += OnFullscreenToggle; gameForm.DisableFullScreen += OnDisableFullScreen; gameForm.FormClosing += OnClosing; + gameForm.DpiChanged += OnDpiChanged; } else { @@ -273,12 +274,12 @@ public override bool Visible } /// - public override double Opacity + public override double Opacity { get { return form?.Opacity ?? 1.0d; - } + } set { if (form != null) @@ -287,7 +288,7 @@ public override double Opacity } } } - + public override Int2 Position { get @@ -409,6 +410,14 @@ public override bool Focused } } + private void OnDpiChanged(object sender, DpiChangedEventArgs e) + { + Dpi = new Int2(e.DeviceDpiNew); + DpiScale = e.DeviceDpiNew / 96.0f; + + base.OnDpiChanged(sender, e); + } + protected override void Destroy() { if (Control != null) diff --git a/sources/engine/Stride.Games/GameWindow.cs b/sources/engine/Stride.Games/GameWindow.cs index 26a1b83937..fb9cce3d34 100644 --- a/sources/engine/Stride.Games/GameWindow.cs +++ b/sources/engine/Stride.Games/GameWindow.cs @@ -2,17 +2,17 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. // // Copyright (c) 2010-2013 SharpDX - Alexandre Mutel -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -72,6 +72,11 @@ public abstract class GameWindow : ComponentBase /// public event EventHandler FullscreenChanged; + /// + /// Occurs when the DPI configuration of this window has changed. + /// + public event EventHandler DpiChanged; + /// /// Occurs before the window gets destroyed. /// @@ -127,7 +132,7 @@ public abstract class GameWindow : ComponentBase /// /// true if visible; otherwise, false. public abstract bool Visible { get; set; } - + /// /// Gets or sets the opacity of the window. /// @@ -174,7 +179,7 @@ public string Title /// /// The size the window should have when switching from fullscreen to windowed mode. /// To get the current actual size use . - /// This gets overwritten when the user resizes the window. + /// This gets overwritten when the user resizes the window. /// public Int2 PreferredWindowedSize { get; set; } = new Int2(768, 432); @@ -215,6 +220,18 @@ internal void SetIsReallyFullscreen(bool isReallyFullscreen) isFullscreen = isReallyFullscreen; } + /// + /// Gets the DPI scale factor of the display where this window is currently displayed, + /// which is used to convert between physical pixels and device-independent pixels (DIPs). + /// + public float DpiScale { get; protected set; } = 1.0f; // 100 % = 96 DPI + + /// + /// Gets the dots per inch (DPI) of the display where this window is currently displayed + /// in the horizontal and vertical directions. + /// + public Int2 Dpi { get; protected set; } = new Int2(96); // 96 DPI (baseline DPI for Windows and many other platforms) + #endregion #region Public Methods and Operators @@ -241,7 +258,7 @@ public void EndScreenDeviceChange() internal Action RunCallback; internal Action ExitCallback; - + private bool isFullscreen; internal abstract void Run(); @@ -284,9 +301,9 @@ protected void OnClientSizeChanged(object source, EventArgs e) { if (!isFullscreen) { - // Update preferred windowed size in windowed mode + // Update preferred windowed size in windowed mode var resizeSize = ClientBounds.Size; - PreferredWindowedSize = new Int2(resizeSize.Width, resizeSize.Height); + PreferredWindowedSize = new Int2(resizeSize.Width, resizeSize.Height); } var handler = ClientSizeChanged; handler?.Invoke(this, e); @@ -316,6 +333,12 @@ protected void OnDisableFullScreen(object source, EventArgs e) IsFullscreen = false; } + protected void OnDpiChanged(object source, EventArgs e) + { + var handler = DpiChanged; + handler?.Invoke(this, e); + } + protected void OnClosing(object source, EventArgs e) { var handler = Closing; diff --git a/sources/engine/Stride.Games/SDL/GameFormSDL.cs b/sources/engine/Stride.Games/SDL/GameFormSDL.cs index f43b141262..d0ca7c2d71 100644 --- a/sources/engine/Stride.Games/SDL/GameFormSDL.cs +++ b/sources/engine/Stride.Games/SDL/GameFormSDL.cs @@ -15,7 +15,8 @@ namespace Stride.Games /// public class GameFormSDL : Window { -#region Initialization + #region Initialization + /// /// Initializes a new instance of the 'GameForm' class. /// @@ -39,9 +40,11 @@ public GameFormSDL(string text) : base(text) RestoredActions += GameForm_RestoredActions; KeyDownActions += GameFormSDL_KeyDownActions; } + #endregion #region Events + /// /// Occurs when [app activated]. /// @@ -75,6 +78,7 @@ public GameFormSDL(string text) : base(text) #endregion #region Implementation + // TODO: The code below is taken from GameForm.cs of the Windows Desktop implementation. This needs reviewing private FormWindowState previousWindowState; diff --git a/sources/engine/Stride.Games/SDL/GameWindowSDL.cs b/sources/engine/Stride.Games/SDL/GameWindowSDL.cs index d169750cf8..86a8df1ea0 100644 --- a/sources/engine/Stride.Games/SDL/GameWindowSDL.cs +++ b/sources/engine/Stride.Games/SDL/GameWindowSDL.cs @@ -109,7 +109,7 @@ protected override void Initialize(GameContext gameContext) window.ClientSize = new Size2(width, height); - window.MouseEnterActions += WindowOnMouseEnterActions; + window.MouseEnterActions += WindowOnMouseEnterActions; window.MouseLeaveActions += WindowOnMouseLeaveActions; var gameForm = window as GameFormSDL; @@ -120,7 +120,7 @@ protected override void Initialize(GameContext gameContext) gameForm.SizeChanged += OnClientSizeChanged; gameForm.CloseActions += GameForm_CloseActions; gameForm.FullscreenToggle += OnFullscreenToggle; - + gameForm.DisplayChangedActions += WindowOnDisplayChangedActions; } else { @@ -380,6 +380,14 @@ public override bool Focused } } + private void WindowOnDisplayChangedActions(WindowEvent sdlWindowEvent) + { + Dpi = new Int2((int) window.Dpi); + DpiScale = window.DpiScale; + + OnDpiChanged(this, EventArgs.Empty); + } + protected override void Destroy() { if (window != null) diff --git a/sources/engine/Stride.Graphics/Direct3D/GraphicsOutput.Direct3D.cs b/sources/engine/Stride.Graphics/Direct3D/GraphicsOutput.Direct3D.cs index 5762cedc33..e69e5d6dd4 100644 --- a/sources/engine/Stride.Graphics/Direct3D/GraphicsOutput.Direct3D.cs +++ b/sources/engine/Stride.Graphics/Direct3D/GraphicsOutput.Direct3D.cs @@ -173,6 +173,18 @@ public sealed unsafe partial class GraphicsOutput /// public Vector2 WhitePoint { get; } + /// + /// Gets the DPI scale factor of the display attached to this output, which is used + /// to convert between physical pixels and device-independent pixels (DIPs). + /// + public float DpiScale { get; } + + /// + /// Gets the dots per inch (DPI) of the display attached to this output + /// in the horizontal and vertical directions. + /// + public Int2 Dpi { get; } + /// /// Initializes a new instance of . @@ -221,6 +233,10 @@ internal GraphicsOutput(GraphicsAdapter adapter, ComPtr nativeOutpu _ => DisplayRotation.Default }; + GetDpiForMonitor(MonitorHandle, out int dpiX, out int dpiY); + Dpi = new Int2(dpiX, dpiY); + DpiScale = dpiX / 96.0f; + if (dxgiOutputVersion >= 6) { var nativeOutput6 = nativeOutput.AsComPtrUnsafe(); @@ -312,6 +328,31 @@ static string GetFriendlyName(char* deviceName) } return null; } + + // + // Attempts to get the DPI for the monitor associated with this output using Win32 API. + // + static void GetDpiForMonitor(nint hMonitor, out int dpiX, out int dpiY) + { + uint x, y; + + // Windows 8.1+: shcore!GetDpiForMonitor + // TODO: Consider using GetDpiForWindow (Windows 10, version 1607+) as a fallback, which can be more accurate in multi-monitor setups with different DPI settings. + // TODO: Consider a fallback to GetDeviceCaps(LOGPIXELSX/Y) via a DC if GetDpiForMonitor is not available, which is the traditional way to get DPI on older Windows versions. + HResult result = Win32.GetDpiForMonitor(hMonitor, Win32.MDT_EFFECTIVE_DPI, &x, &y); + + if (result.IsSuccess) + { + dpiX = (int) x; + dpiY = (int) y; + } + else + { + // Defaults to 96 dpi, which corresponds to 100% scaling + dpiX = 96; + dpiY = 96; + } + } } /// diff --git a/sources/engine/Stride.Graphics/SDL/Window.cs b/sources/engine/Stride.Graphics/SDL/Window.cs index c59d300de7..4bf73708be 100644 --- a/sources/engine/Stride.Graphics/SDL/Window.cs +++ b/sources/engine/Stride.Graphics/SDL/Window.cs @@ -15,7 +15,7 @@ public unsafe class Window : IDisposable private Silk.NET.SDL.Window* sdlHandle; -#region Initialization + #region Initialization private static readonly object initLock = new(); @@ -95,11 +95,10 @@ public Window(string title, IntPtr parent) // Create the SDL window and then extract the native handle. sdlHandle = SDL.CreateWindow(title, Sdl.WindowposUndefined, Sdl.WindowposUndefined, 640, 480, (uint)flags); } - if (sdlHandle == null) { - throw new Exception("Cannot allocate SDL Window: " + SDL.GetErrorS()); + throw new Exception("Cannot allocate SDL Window: " + SDL.GetErrorS()); } SysWMInfo info = default; @@ -127,6 +126,13 @@ public Window(string title, IntPtr parent) { Handle = (IntPtr)info.Info.Cocoa.Window; } + + var displayIndex = SDL.GetWindowDisplayIndex(sdlHandle); + if (displayIndex == -1) + displayIndex = 0; + + DisplayIndex = displayIndex; + Application.RegisterWindow(this); Application.ProcessEvents(); } @@ -343,7 +349,7 @@ public Size2 Size } set { SDL.SetWindowSize(sdlHandle, value.Width, value.Height); } } - + /// /// The opacity of the window. /// @@ -468,6 +474,30 @@ public FormBorderStyle FormBorderStyle } } + /// + /// DPI (dots per inch) of the Window. + /// + public float Dpi + { + get + { + float ddpi, hdpi, vdpi; + + if (SDL.GetDisplayDPI(DisplayIndex, &ddpi, &hdpi, &vdpi) != 0) + { + // Failed to get DPI, return a default value of 96 which is the standard DPI for many platforms. + return 96.0f; + } + + return ddpi; + } + } + + /// + /// DPI scaling factor of the Window. 100 % corresponds to a DPI of 96. + /// + public float DpiScale => Dpi / 96.0f; + public void SetRelativeMouseMode(bool enabled) { SDL.SetRelativeMouseMode(enabled ? SdlBool.True : SdlBool.False); @@ -511,6 +541,7 @@ public void SetRelativeMouseMode(bool enabled) public event WindowEventDelegate MouseLeaveActions; public event WindowEventDelegate FocusGainedActions; public event WindowEventDelegate FocusLostActions; + public event WindowEventDelegate DisplayChangedActions; public event DropEventDelegate DropFileActions; /// @@ -576,7 +607,7 @@ public virtual void ProcessEvent(Event e) case EventType.Fingerup: FingerReleaseActions?.Invoke(e.Tfinger); break; - + case EventType.Dropfile: DropFileActions?.Invoke(Silk.NET.Core.Native.SilkMarshal.PtrToString((IntPtr)e.Drop.File, Silk.NET.Core.Native.NativeStringEncoding.UTF8)); break; @@ -632,12 +663,27 @@ public virtual void ProcessEvent(Event e) case WindowEventID.FocusLost: FocusLostActions?.Invoke(e.Window); break; - } + + case WindowEventID.DisplayChanged: + OnDisplayChanged(e.Window); + break; + } break; } } } + private void OnDisplayChanged(WindowEvent e) + { + var displayIndex = SDL.GetWindowDisplayIndex(sdlHandle); + if (displayIndex == -1) + displayIndex = 0; + + DisplayIndex = displayIndex; + + DisplayChangedActions?.Invoke(e); + } + /// /// Platform specific handle for Window. /// @@ -654,6 +700,11 @@ public virtual void ProcessEvent(Event e) /// public IntPtr Display { get; private set; } + /// + /// Index of the display where the current Window is being shown. + /// + public int DisplayIndex { get; private set; } + /// /// Surface of current Window (valid only for Android). /// @@ -672,7 +723,8 @@ public bool Exists get { return SdlHandle != IntPtr.Zero; } } -#region Disposal + #region Disposal + ~Window() { Dispose(false); @@ -712,7 +764,7 @@ protected virtual void Dispose(bool disposing) Handle = IntPtr.Zero; } } - + // This code added to correctly implement the disposable pattern. public void Dispose() { diff --git a/sources/engine/Stride.Graphics/Direct3D/Win32.cs b/sources/engine/Stride.Graphics/Win32.cs similarity index 88% rename from sources/engine/Stride.Graphics/Direct3D/Win32.cs rename to sources/engine/Stride.Graphics/Win32.cs index 7de6889d51..28158fde3b 100644 --- a/sources/engine/Stride.Graphics/Direct3D/Win32.cs +++ b/sources/engine/Stride.Graphics/Win32.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -#if STRIDE_GRAPHICS_API_DIRECT3D11 || STRIDE_GRAPHICS_API_DIRECT3D12 +#if STRIDE_PLATFORM_WINDOWS using System; using System.Runtime.CompilerServices; @@ -39,6 +39,13 @@ public struct DISPLAY_DEVICEW [DllImport("user32", ExactSpelling = true)] public static unsafe extern BOOL EnumDisplayDevicesW(char* lpDevice, uint iDevNum, DISPLAY_DEVICEW* lpDisplayDevice, uint dwFlags); + // MONITOR_DPI_TYPE + public const int MDT_EFFECTIVE_DPI = 0; + + // GetDpiForMonitor(HMONITOR hmonitor, MONITOR_DPI_TYPE dpiType, UINT* dpiX, UINT* dpiY); + [DllImport("shcore", ExactSpelling = true)] + public static unsafe extern int GetDpiForMonitor(nint hmonitor, int dpiType, uint* dpiX, uint* dpiY); + #region Helper structs and types public readonly struct BOOL(int value) diff --git a/sources/sdk/Stride.Build.Sdk/Sdk/Stride.Platform.targets b/sources/sdk/Stride.Build.Sdk/Sdk/Stride.Platform.targets index 8f5d8a7687..f6982b173a 100644 --- a/sources/sdk/Stride.Build.Sdk/Sdk/Stride.Platform.targets +++ b/sources/sdk/Stride.Build.Sdk/Sdk/Stride.Platform.targets @@ -15,8 +15,12 @@ - + STRIDE_PLATFORM_DESKTOP + + $(StridePlatformDefines);STRIDE_PLATFORM_WINDOWS + $(StridePlatformDefines);STRIDE_PLATFORM_LINUX + $(StridePlatformDefines);STRIDE_PLATFORM_MACOS