From fd64ad7c89a4d393daa93521818979416f612e48 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 25 Dec 2019 18:49:47 +0000 Subject: [PATCH] Support rendering at non-integer display scales: * 2x and 3x DPI artwork can be specified using Image2x and Image3x in chrome.yaml. * Images are rendered using bilinear interpolation. * For non-integer screen scales, prefer downscaling the next biggest resolution image over upscaling. --- OpenRA.Game/Graphics/ChromeProvider.cs | 71 ++++++++++++++++--- OpenRA.Game/Graphics/Sheet.cs | 1 + OpenRA.Game/Graphics/Sprite.cs | 16 ++--- OpenRA.Game/PlayerDatabase.cs | 1 + OpenRA.Game/Renderer.cs | 2 + .../LoadScreens/SheetLoadScreen.cs | 36 +++++++++- mods/cnc/chrome.yaml | 2 + mods/cnc/mod.yaml | 2 + mods/d2k/chrome.yaml | 2 + mods/d2k/mod.yaml | 2 + mods/modcontent/chrome.yaml | 2 + mods/modcontent/mod.yaml | 2 + mods/ra/chrome.yaml | 4 ++ mods/ra/mod.yaml | 2 + mods/ts/chrome.yaml | 2 + 15 files changed, 130 insertions(+), 17 deletions(-) diff --git a/OpenRA.Game/Graphics/ChromeProvider.cs b/OpenRA.Game/Graphics/ChromeProvider.cs index 1a5f73eaff..5a4b6f2995 100644 --- a/OpenRA.Game/Graphics/ChromeProvider.cs +++ b/OpenRA.Game/Graphics/ChromeProvider.cs @@ -44,6 +44,9 @@ namespace OpenRA.Graphics public class Collection { public readonly string Image = null; + public readonly string Image2x = null; + public readonly string Image3x = null; + public readonly int[] PanelRegion = null; public readonly PanelSides PanelSides = PanelSides.All; public readonly Dictionary Regions = new Dictionary(); @@ -54,18 +57,25 @@ namespace OpenRA.Graphics static Dictionary cachedSheets; static Dictionary> cachedSprites; static Dictionary cachedPanelSprites; + static Dictionary cachedCollectionSheets; static IReadOnlyFileSystem fileSystem; + static float dpiScale = 1; public static void Initialize(ModData modData) { Deinitialize(); + // Load higher resolution images if available on HiDPI displays + if (Game.Renderer != null) + dpiScale = Game.Renderer.WindowScale; + fileSystem = modData.DefaultFileSystem; collections = new Dictionary(); cachedSheets = new Dictionary(); cachedSprites = new Dictionary>(); cachedPanelSprites = new Dictionary(); + cachedCollectionSheets = new Dictionary(); Collections = new ReadOnlyDictionary(collections); @@ -87,6 +97,7 @@ namespace OpenRA.Graphics cachedSheets = null; cachedSprites = null; cachedPanelSprites = null; + cachedCollectionSheets = null; } static void LoadCollection(string name, MiniYaml yaml) @@ -99,16 +110,42 @@ namespace OpenRA.Graphics static Sheet SheetForCollection(Collection c) { - // Cached sheet Sheet sheet; - if (cachedSheets.ContainsKey(c.Image)) - sheet = cachedSheets[c.Image]; - else - { - using (var stream = fileSystem.Open(c.Image)) - sheet = new Sheet(SheetType.BGRA, stream); - cachedSheets.Add(c.Image, sheet); + // Outer cache avoids recalculating image names + if (!cachedCollectionSheets.TryGetValue(c, out sheet)) + { + string sheetImage; + float sheetScale; + if (dpiScale > 2 && !string.IsNullOrEmpty(c.Image3x)) + { + sheetImage = c.Image3x; + sheetScale = 3; + } + else if (dpiScale > 1 && !string.IsNullOrEmpty(c.Image2x)) + { + sheetImage = c.Image2x; + sheetScale = 2; + } + else + { + sheetImage = c.Image; + sheetScale = 1; + } + + // Inner cache makes sure we share sheets between collections + if (!cachedSheets.TryGetValue(sheetImage, out sheet)) + { + using (var stream = fileSystem.Open(sheetImage)) + sheet = new Sheet(SheetType.BGRA, stream); + + sheet.GetTexture().ScaleFilter = TextureScaleFilter.Linear; + sheet.DPIScale = sheetScale; + + cachedSheets.Add(sheetImage, sheet); + } + + cachedCollectionSheets.Add(c, sheet); } return sheet; @@ -239,5 +276,23 @@ namespace OpenRA.Graphics var pr = collection.PanelRegion; return new Size(pr[2] + pr[6], pr[3] + pr[7]); } + + public static void SetDPIScale(float scale) + { + if (dpiScale == scale) + return; + + dpiScale = scale; + + // Clear the sprite caches so the new artwork can be loaded + // Sheets are not cleared: we assume that the extra memory overhead + // of having the same sheet in memory in multiple DPIs is better than + // the overhead of having to dispose and reload everything. + // Changing the DPI scale is rare, but if it does happen then there + // is a reasonable chance that it may happen again this session. + cachedSprites.Clear(); + cachedPanelSprites.Clear(); + cachedCollectionSheets.Clear(); + } } } diff --git a/OpenRA.Game/Graphics/Sheet.cs b/OpenRA.Game/Graphics/Sheet.cs index 51da5762f5..3a98412476 100644 --- a/OpenRA.Game/Graphics/Sheet.cs +++ b/OpenRA.Game/Graphics/Sheet.cs @@ -25,6 +25,7 @@ namespace OpenRA.Graphics public readonly Size Size; public readonly SheetType Type; + public float DPIScale = 1f; public byte[] GetData() { diff --git a/OpenRA.Game/Graphics/Sprite.cs b/OpenRA.Game/Graphics/Sprite.cs index 03d3677c2b..506dfe46f4 100644 --- a/OpenRA.Game/Graphics/Sprite.cs +++ b/OpenRA.Game/Graphics/Sprite.cs @@ -41,10 +41,10 @@ namespace OpenRA.Graphics FractionalOffset = Size.Z != 0 ? offset / Size : new float3(offset.X / Size.X, offset.Y / Size.Y, 0); - Left = (float)Math.Min(bounds.Left, bounds.Right) / sheet.Size.Width; - Top = (float)Math.Min(bounds.Top, bounds.Bottom) / sheet.Size.Height; - Right = (float)Math.Max(bounds.Left, bounds.Right) / sheet.Size.Width; - Bottom = (float)Math.Max(bounds.Top, bounds.Bottom) / sheet.Size.Height; + Left = (float)Math.Min(bounds.Left, bounds.Right) * sheet.DPIScale / sheet.Size.Width; + Top = (float)Math.Min(bounds.Top, bounds.Bottom) * sheet.DPIScale / sheet.Size.Height; + Right = (float)Math.Max(bounds.Left, bounds.Right) * sheet.DPIScale / sheet.Size.Width; + Bottom = (float)Math.Max(bounds.Top, bounds.Bottom) * sheet.DPIScale / sheet.Size.Height; } } @@ -61,10 +61,10 @@ namespace OpenRA.Graphics SecondarySheet = secondarySheet; SecondaryBounds = secondaryBounds; SecondaryChannel = secondaryChannel; - SecondaryLeft = (float)Math.Min(secondaryBounds.Left, secondaryBounds.Right) / s.Sheet.Size.Width; - SecondaryTop = (float)Math.Min(secondaryBounds.Top, secondaryBounds.Bottom) / s.Sheet.Size.Height; - SecondaryRight = (float)Math.Max(secondaryBounds.Left, secondaryBounds.Right) / s.Sheet.Size.Width; - SecondaryBottom = (float)Math.Max(secondaryBounds.Top, secondaryBounds.Bottom) / s.Sheet.Size.Height; + SecondaryLeft = (float)Math.Min(secondaryBounds.Left, secondaryBounds.Right) * secondarySheet.DPIScale / s.Sheet.Size.Width; + SecondaryTop = (float)Math.Min(secondaryBounds.Top, secondaryBounds.Bottom) * secondarySheet.DPIScale / s.Sheet.Size.Height; + SecondaryRight = (float)Math.Max(secondaryBounds.Left, secondaryBounds.Right) * secondarySheet.DPIScale / s.Sheet.Size.Width; + SecondaryBottom = (float)Math.Max(secondaryBounds.Top, secondaryBounds.Bottom) * secondarySheet.DPIScale / s.Sheet.Size.Height; } } diff --git a/OpenRA.Game/PlayerDatabase.cs b/OpenRA.Game/PlayerDatabase.cs index ed4bccbea1..7a058ef08f 100644 --- a/OpenRA.Game/PlayerDatabase.cs +++ b/OpenRA.Game/PlayerDatabase.cs @@ -57,6 +57,7 @@ namespace OpenRA if (!spriteCache.TryGetValue(icon24Node.Value.Value, out sprite)) { sprite = spriteCache[icon24Node.Value.Value] = sheetBuilder.Allocate(new Size(24, 24)); + sprite.Sheet.GetTexture().ScaleFilter = TextureScaleFilter.Linear; Action onComplete = i => { diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index db4058ee6b..16b149981a 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -110,6 +110,8 @@ namespace OpenRA { Game.RunAfterTick(() => { + ChromeProvider.SetDPIScale(after); + foreach (var f in Fonts) f.Value.SetScale(after); }); diff --git a/OpenRA.Mods.Common/LoadScreens/SheetLoadScreen.cs b/OpenRA.Mods.Common/LoadScreens/SheetLoadScreen.cs index 6d276c340d..1033112efb 100644 --- a/OpenRA.Mods.Common/LoadScreens/SheetLoadScreen.cs +++ b/OpenRA.Mods.Common/LoadScreens/SheetLoadScreen.cs @@ -20,6 +20,7 @@ namespace OpenRA.Mods.Common.LoadScreens Stopwatch lastUpdate; protected Dictionary Info { get; private set; } + float dpiScale = 1; Sheet sheet; public override void Init(ModData modData, Dictionary info) @@ -40,9 +41,42 @@ namespace OpenRA.Mods.Common.LoadScreens if (lastUpdate == null) lastUpdate = Stopwatch.StartNew(); + // Check for window DPI changes + // We can't trust notifications to be working during initialization, so must do this manually + var scale = Game.Renderer.WindowScale; + if (dpiScale != scale) + { + dpiScale = scale; + + // Force images to be reloaded on the next display + if (sheet != null) + sheet.Dispose(); + + sheet = null; + } + if (sheet == null && Info.ContainsKey("Image")) - using (var stream = ModData.DefaultFileSystem.Open(Info["Image"])) + { + var key = "Image"; + float sheetScale = 1; + if (dpiScale > 2 && Info.ContainsKey("Image3x")) + { + key = "Image3x"; + sheetScale = 3; + } + else if (dpiScale > 1 && Info.ContainsKey("Image2x")) + { + key = "Image2x"; + sheetScale = 2; + } + + using (var stream = ModData.DefaultFileSystem.Open(Info[key])) + { sheet = new Sheet(SheetType.BGRA, stream); + sheet.GetTexture().ScaleFilter = TextureScaleFilter.Linear; + sheet.DPIScale = sheetScale; + } + } Game.Renderer.BeginUI(); DisplayInner(Game.Renderer, sheet); diff --git a/mods/cnc/chrome.yaml b/mods/cnc/chrome.yaml index d29416e060..1650a6f467 100644 --- a/mods/cnc/chrome.yaml +++ b/mods/cnc/chrome.yaml @@ -1,5 +1,7 @@ ^Chrome: Image: chrome.png + Image2x: chrome-2x.png + Image3x: chrome-3x.png logos: Inherits: ^Chrome diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index b7ebf10a4f..f86fb6348b 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -153,6 +153,8 @@ Hotkeys: LoadScreen: CncLoadScreen Image: cnc|uibits/chrome.png + Image2x: cnc|uibits/chrome-2x.png + Image3x: cnc|uibits/chrome-3x.png Text: Loading ServerTraits: diff --git a/mods/d2k/chrome.yaml b/mods/d2k/chrome.yaml index 118a944b52..130c4db261 100644 --- a/mods/d2k/chrome.yaml +++ b/mods/d2k/chrome.yaml @@ -6,6 +6,8 @@ ^Glyphs: Image: glyphs.png + Image2x: glyphs-2x.png + Image3x: glyphs-3x.png ^LoadScreen: Image: loadscreen.png diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index d7a8dcba66..87dd3fa9ea 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -139,6 +139,8 @@ Hotkeys: LoadScreen: LogoStripeLoadScreen Image: d2k|uibits/loadscreen.png + Image2x: d2k|uibits/loadscreen-2x.png + Image3x: d2k|uibits/loadscreen-3x.png Text: Filling Crates..., Breeding Sandworms..., Fuelling carryalls..., Deploying harvesters..., Preparing thopters..., Summoning mentats... ServerTraits: diff --git a/mods/modcontent/chrome.yaml b/mods/modcontent/chrome.yaml index 718c9ba8f7..61605cbf77 100644 --- a/mods/modcontent/chrome.yaml +++ b/mods/modcontent/chrome.yaml @@ -1,5 +1,7 @@ ^Chrome: Image: chrome.png + Image2x: chrome-2x.png + Image3x: chrome-3x.png panel-header: Inherits: ^Chrome diff --git a/mods/modcontent/mod.yaml b/mods/modcontent/mod.yaml index feca980b10..2e4f1fdfd1 100644 --- a/mods/modcontent/mod.yaml +++ b/mods/modcontent/mod.yaml @@ -28,6 +28,8 @@ Notifications: LoadScreen: ModContentLoadScreen Image: ./mods/modcontent/chrome.png + Image2x: ./mods/modcontent/chrome-2x.png + Image3x: ./mods/modcontent/chrome-3x.png ChromeMetrics: common|metrics.yaml diff --git a/mods/ra/chrome.yaml b/mods/ra/chrome.yaml index 77d04b091e..f6160d97ca 100644 --- a/mods/ra/chrome.yaml +++ b/mods/ra/chrome.yaml @@ -6,9 +6,13 @@ ^Glyphs: Image: glyphs.png + Image2x: glyphs-2x.png + Image3x: glyphs-3x.png ^LoadScreen: Image: loadscreen.png + Image2x: loadscreen-2x.png + Image3x: loadscreen-3x.png sidebar-allies: Inherits: ^Sidebar diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 6c67da9062..1abded59e0 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -155,6 +155,8 @@ Hotkeys: LoadScreen: LogoStripeLoadScreen Image: ra|uibits/loadscreen.png + Image2x: ra|uibits/loadscreen-2x.png + Image3x: ra|uibits/loadscreen-3x.png Text: Filling Crates..., Charging Capacitors..., Reticulating Splines..., Planting Trees..., Building Bridges..., Aging Empires..., Compiling EVA..., Constructing Pylons..., Activating Skynet..., Splitting Atoms... ServerTraits: diff --git a/mods/ts/chrome.yaml b/mods/ts/chrome.yaml index bb44517c5f..d93e193de5 100644 --- a/mods/ts/chrome.yaml +++ b/mods/ts/chrome.yaml @@ -9,6 +9,8 @@ ^Glyphs: Image: glyphs.png + Image2x: glyphs-2x.png + Image3x: glyphs-3x.png ^LoadScreen: Image: loadscreen.png