diff --git a/OpenRA.Game/Graphics/CursorManager.cs b/OpenRA.Game/Graphics/CursorManager.cs index 20a9320ad3..db922b0388 100644 --- a/OpenRA.Game/Graphics/CursorManager.cs +++ b/OpenRA.Game/Graphics/CursorManager.cs @@ -193,14 +193,13 @@ namespace OpenRA.Graphics // Cursor is rendered in native window coordinates // Apply same scaling rules as hardware cursors - var ws = Game.Renderer.WindowScale; - if (ws > 1.5f) + if (Game.Renderer.NativeWindowScale > 1.5f) cursorSize = 2 * cursorSize; var mousePos = isLocked ? lockedPosition : Viewport.LastMousePos; renderer.RgbaSpriteRenderer.DrawSprite(cursorSprite, mousePos, - cursorSize / ws); + cursorSize / Game.Renderer.WindowScale); } public void Lock() diff --git a/OpenRA.Game/Graphics/PlatformInterfaces.cs b/OpenRA.Game/Graphics/PlatformInterfaces.cs index 3e8b7d22b0..5fe42212fe 100644 --- a/OpenRA.Game/Graphics/PlatformInterfaces.cs +++ b/OpenRA.Game/Graphics/PlatformInterfaces.cs @@ -17,7 +17,7 @@ namespace OpenRA { public interface IPlatform { - IPlatformWindow CreateWindow(Size size, WindowMode windowMode, int batchSize); + IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize); ISoundEngine CreateSound(string device); IFont CreateFont(byte[] data); } @@ -39,11 +39,13 @@ namespace OpenRA { IGraphicsContext Context { get; } - Size WindowSize { get; } - float WindowScale { get; } + Size NativeWindowSize { get; } + Size EffectiveWindowSize { get; } + float NativeWindowScale { get; } + float EffectiveWindowScale { get; } Size SurfaceSize { get; } - event Action OnWindowScaleChanged; + event Action OnWindowScaleChanged; void PumpInput(IInputHandler inputHandler); string GetClipboardText(); diff --git a/OpenRA.Game/Graphics/Viewport.cs b/OpenRA.Game/Graphics/Viewport.cs index b5815d5050..c1f5cbd0d7 100644 --- a/OpenRA.Game/Graphics/Viewport.cs +++ b/OpenRA.Game/Graphics/Viewport.cs @@ -78,7 +78,7 @@ namespace OpenRA.Graphics private set { zoom = value; - viewportSize = (1f / zoom * new float2(Game.Renderer.Resolution)).ToInt2(); + viewportSize = (1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2(); cellsDirty = true; allCellsDirty = true; } @@ -181,7 +181,7 @@ namespace OpenRA.Graphics float CalculateMinimumZoom(float minHeight, float maxHeight) { - var h = Game.Renderer.Resolution.Height; + var h = Game.Renderer.NativeResolution.Height; // Check the easy case: the native resolution is within the maximum limit // Also catches the case where the user may force a resolution smaller than the minimum window size @@ -222,7 +222,7 @@ namespace OpenRA.Graphics minZoom = CalculateMinimumZoom(range.X, range.Y); } - maxZoom = Math.Min(minZoom * viewportSizes.MaxZoomScale, Game.Renderer.Resolution.Height * 1f / viewportSizes.MaxZoomWindowHeight); + maxZoom = Math.Min(minZoom * viewportSizes.MaxZoomScale, Game.Renderer.NativeResolution.Height * 1f / viewportSizes.MaxZoomWindowHeight); if (unlockMinZoom) { @@ -304,9 +304,9 @@ namespace OpenRA.Graphics yield return new MPos(u, v); } - public int2 ViewToWorldPx(int2 view) { return (1f / Zoom * view.ToFloat2()).ToInt2() + TopLeft; } - public int2 WorldToViewPx(int2 world) { return (Zoom * (world - TopLeft).ToFloat2()).ToInt2(); } - public int2 WorldToViewPx(float3 world) { return (Zoom * (world - TopLeft).XY).ToInt2(); } + public int2 ViewToWorldPx(int2 view) { return (graphicSettings.UIScale / Zoom * view.ToFloat2()).ToInt2() + TopLeft; } + public int2 WorldToViewPx(int2 world) { return ((Zoom / graphicSettings.UIScale) * (world - TopLeft).ToFloat2()).ToInt2(); } + public int2 WorldToViewPx(float3 world) { return ((Zoom / graphicSettings.UIScale) * (world - TopLeft).XY).ToInt2(); } public void Center(IEnumerable actors) { diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index 16b149981a..cc10626535 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -66,7 +66,7 @@ namespace OpenRA this.platform = platform; var resolution = GetResolution(graphicSettings); - Window = platform.CreateWindow(new Size(resolution.Width, resolution.Height), graphicSettings.Mode, graphicSettings.BatchSize); + Window = platform.CreateWindow(new Size(resolution.Width, resolution.Height), graphicSettings.Mode, graphicSettings.UIScale, graphicSettings.BatchSize); Context = Window.Context; TempBufferSize = graphicSettings.BatchSize; @@ -103,17 +103,17 @@ namespace OpenRA fontSheetBuilder = new SheetBuilder(SheetType.BGRA, 512); Fonts = modData.Manifest.Get().FontList.ToDictionary(x => x.Key, x => new SpriteFont(x.Value.Font, modData.DefaultFileSystem.Open(x.Value.Font).ReadAllBytes(), - x.Value.Size, x.Value.Ascender, Window.WindowScale, fontSheetBuilder)).AsReadOnly(); + x.Value.Size, x.Value.Ascender, Window.EffectiveWindowScale, fontSheetBuilder)).AsReadOnly(); } - Window.OnWindowScaleChanged += (before, after) => + Window.OnWindowScaleChanged += (oldNative, oldEffective, newNative, newEffective) => { Game.RunAfterTick(() => { - ChromeProvider.SetDPIScale(after); + ChromeProvider.SetDPIScale(newEffective); foreach (var f in Fonts) - f.Value.SetScale(after); + f.Value.SetScale(newEffective); }); }; } @@ -159,7 +159,7 @@ namespace OpenRA // but to have a higher resolution backing surface with more than 1 texture pixel per viewport pixel. // We must convert the surface buffer size to a viewport size - in general this is NOT just the window size // rounded to the next power of two, as the NextPowerOf2 calculation is done in the surface pixel coordinates - var scale = Window.WindowScale; + var scale = Window.EffectiveWindowScale; var bufferSize = new Size((int)(surfaceBufferSize.Width / scale), (int)(surfaceBufferSize.Height / scale)); if (lastBufferSize != bufferSize) { @@ -220,7 +220,7 @@ namespace OpenRA // Render the world buffer into the UI buffer screenBuffer.Bind(); - var scale = Window.WindowScale; + var scale = Window.EffectiveWindowScale; var bufferSize = new Size((int)(screenSprite.Bounds.Width / scale), (int)(-screenSprite.Bounds.Height / scale)); SpriteRenderer.SetAntialiasingPixelsPerTexel(Window.SurfaceSize.Height * 1f / worldSprite.Bounds.Height); @@ -292,8 +292,10 @@ namespace OpenRA CurrentBatchRenderer = null; } - public Size Resolution { get { return Window.WindowSize; } } - public float WindowScale { get { return Window.WindowScale; } } + public Size Resolution { get { return Window.EffectiveWindowSize; } } + public Size NativeResolution { get { return Window.NativeWindowSize; } } + public float WindowScale { get { return Window.EffectiveWindowScale; } } + public float NativeWindowScale { get { return Window.NativeWindowScale; } } public interface IBatchRenderer { void Flush(); } @@ -385,7 +387,7 @@ namespace OpenRA throw new InvalidOperationException("EndFrame called with renderType = {0}, expected RenderType.UI.".F(renderType)); Flush(); - SpriteRenderer.SetAntialiasingPixelsPerTexel(Window.WindowScale); + SpriteRenderer.SetAntialiasingPixelsPerTexel(Window.EffectiveWindowScale); } public void DisableAntialiasingFilter() diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index a4c7a167c2..2071f0d11e 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -156,6 +156,7 @@ namespace OpenRA public bool CursorDouble = false; public WorldViewport ViewportDistance = WorldViewport.Medium; + public float UIScale = 1; [Desc("Add a frame rate limiter.")] public bool CapFramerate = false; diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index 9721ed0425..2863a39dcb 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -44,7 +44,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic bool newsOpen; // Increment the version number when adding new stats - const int SystemInformationVersion = 3; + const int SystemInformationVersion = 4; Dictionary> GetSystemInformation() { var lang = System.Globalization.CultureInfo.InstalledUICulture.TwoLetterISOLanguageName; @@ -57,8 +57,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic { "x64process", Pair.New("Process is 64 bit", Environment.Is64BitProcess.ToString()) }, { "runtime", Pair.New(".NET Runtime", Platform.RuntimeVersion) }, { "gl", Pair.New("OpenGL Version", Game.Renderer.GLVersion) }, - { "windowsize", Pair.New("Window Size", "{0}x{1}".F(Game.Renderer.Resolution.Width, Game.Renderer.Resolution.Height)) }, - { "windowscale", Pair.New("Window Scale", Game.Renderer.WindowScale.ToString("F2", CultureInfo.InvariantCulture)) }, + { "windowsize", Pair.New("Window Size", "{0}x{1}".F(Game.Renderer.NativeResolution.Width, Game.Renderer.NativeResolution.Height)) }, + { "windowscale", Pair.New("Window Scale", Game.Renderer.NativeWindowScale.ToString("F2", CultureInfo.InvariantCulture)) }, + { "uiscale", Pair.New("UI Scale", Game.Settings.Graphics.UIScale.ToString("F2", CultureInfo.InvariantCulture)) }, { "lang", Pair.New("System Language", lang) } }; } diff --git a/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs index 19b927058a..64228a1b5d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs @@ -841,7 +841,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic }; var viewportSizes = Game.ModData.Manifest.Get(); - var windowHeight = Game.Renderer.Resolution.Height; + var windowHeight = Game.Renderer.NativeResolution.Height; var validSizes = new List() { WorldViewport.Close }; if (viewportSizes.GetSizeRange(WorldViewport.Medium).X < windowHeight) diff --git a/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs b/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs index 4f3e19cb12..360184fdd0 100644 --- a/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs +++ b/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs @@ -39,9 +39,8 @@ namespace OpenRA.Mods.Common.Widgets // Cursor is rendered in native window coordinates // Apply same scaling rules as hardware cursors - var ws = Game.Renderer.WindowScale; - var scale = (graphicSettings.CursorDouble ? 2 : 1) * (ws > 1.5f ? 2 : 1); - WidgetUtils.DrawSHPCentered(sprite, ChildOrigin, directionPalette, scale / ws); + var scale = (graphicSettings.CursorDouble ? 2 : 1) * (Game.Renderer.NativeWindowScale > 1.5f ? 2 : 1); + WidgetUtils.DrawSHPCentered(sprite, ChildOrigin, directionPalette, scale / Game.Renderer.WindowScale); } } diff --git a/OpenRA.Platforms.Default/DefaultPlatform.cs b/OpenRA.Platforms.Default/DefaultPlatform.cs index 790a1f95a0..dde6cf3794 100644 --- a/OpenRA.Platforms.Default/DefaultPlatform.cs +++ b/OpenRA.Platforms.Default/DefaultPlatform.cs @@ -16,9 +16,9 @@ namespace OpenRA.Platforms.Default { public class DefaultPlatform : IPlatform { - public IPlatformWindow CreateWindow(Size size, WindowMode windowMode, int batchSize) + public IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize) { - return new Sdl2PlatformWindow(size, windowMode, batchSize); + return new Sdl2PlatformWindow(size, windowMode, scaleModifier, batchSize); } public ISoundEngine CreateSound(string device) diff --git a/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs b/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs index 9425cb9673..5a5af71c77 100644 --- a/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs +++ b/OpenRA.Platforms.Default/Sdl2GraphicsContext.cs @@ -98,8 +98,8 @@ namespace OpenRA.Platforms.Default if (height < 0) height = 0; - var windowSize = window.WindowSize; - var windowScale = window.WindowScale; + var windowSize = window.EffectiveWindowSize; + var windowScale = window.EffectiveWindowScale; var surfaceSize = window.SurfaceSize; if (windowSize != surfaceSize) diff --git a/OpenRA.Platforms.Default/Sdl2Input.cs b/OpenRA.Platforms.Default/Sdl2Input.cs index 402026177d..40dba07cd6 100644 --- a/OpenRA.Platforms.Default/Sdl2Input.cs +++ b/OpenRA.Platforms.Default/Sdl2Input.cs @@ -44,8 +44,19 @@ namespace OpenRA.Platforms.Default { // On Windows and Linux (X11) events are given in surface coordinates // These must be scaled to our effective window coordinates - if (Platform.CurrentPlatform != PlatformType.OSX && device.WindowSize != device.SurfaceSize) - return new int2((int)(x / device.WindowScale), (int)(y / device.WindowScale)); + // Round fractional components up to avoid rounding small deltas to 0 + if (Platform.CurrentPlatform != PlatformType.OSX && device.EffectiveWindowSize != device.SurfaceSize) + { + var s = 1 / device.EffectiveWindowScale; + return new int2((int)(Math.Sign(x) / 2f + x * s), (int)(Math.Sign(x) / 2f + y * s)); + } + + // On macOS we must still account for the user-requested scale modifier + if (Platform.CurrentPlatform == PlatformType.OSX && device.EffectiveWindowScale != device.NativeWindowScale) + { + var s = device.NativeWindowScale / device.EffectiveWindowScale; + return new int2((int)(Math.Sign(x) / 2f + x * s), (int)(Math.Sign(x) / 2f + y * s)); + } return new int2(x, y); } diff --git a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs index e999d9d394..0c0d1b537d 100644 --- a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs +++ b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs @@ -31,6 +31,7 @@ namespace OpenRA.Platforms.Default Size surfaceSize; float windowScale = 1f; int2? lockedMousePosition; + float scaleModifier; internal IntPtr Window { @@ -41,7 +42,7 @@ namespace OpenRA.Platforms.Default } } - public Size WindowSize + public Size NativeWindowSize { get { @@ -50,7 +51,16 @@ namespace OpenRA.Platforms.Default } } - public float WindowScale + public Size EffectiveWindowSize + { + get + { + lock (syncObject) + return new Size((int)(windowSize.Width / scaleModifier), (int)(windowSize.Height / scaleModifier)); + } + } + + public float NativeWindowScale { get { @@ -59,6 +69,15 @@ namespace OpenRA.Platforms.Default } } + public float EffectiveWindowScale + { + get + { + lock (syncObject) + return windowScale * scaleModifier; + } + } + public Size SurfaceSize { get @@ -68,16 +87,18 @@ namespace OpenRA.Platforms.Default } } - public event Action OnWindowScaleChanged = (before, after) => { }; + public event Action OnWindowScaleChanged = (oldNative, oldEffective, newNative, newEffective) => { }; [DllImport("user32.dll")] static extern bool SetProcessDPIAware(); - public Sdl2PlatformWindow(Size requestEffectiveWindowSize, WindowMode windowMode, int batchSize) + public Sdl2PlatformWindow(Size requestEffectiveWindowSize, WindowMode windowMode, float scaleModifier, int batchSize) { // Lock the Window/Surface properties until initialization is complete lock (syncObject) { + this.scaleModifier = scaleModifier; + // Disable legacy scaling on Windows if (Platform.CurrentPlatform == PlatformType.Windows) SetProcessDPIAware(); @@ -269,7 +290,7 @@ namespace OpenRA.Platforms.Default { // Pixel double the cursor on non-OSX if the window scale is large enough // OSX does this for us automatically - if (Platform.CurrentPlatform != PlatformType.OSX && WindowScale > 1.5) + if (Platform.CurrentPlatform != PlatformType.OSX && NativeWindowScale > 1.5f) { data = DoublePixelData(data, size); size = new Size(2 * size.Width, 2 * size.Height); @@ -341,7 +362,7 @@ namespace OpenRA.Platforms.Default windowScale = width * 1f / windowSize.Width; } - OnWindowScaleChanged(oldScale, windowScale); + OnWindowScaleChanged(oldScale, oldScale * scaleModifier, windowScale, windowScale * scaleModifier); } } }