diff --git a/OpenRA.Game/Graphics/PlatformInterfaces.cs b/OpenRA.Game/Graphics/PlatformInterfaces.cs index 436e445384..4a889ec1a0 100644 --- a/OpenRA.Game/Graphics/PlatformInterfaces.cs +++ b/OpenRA.Game/Graphics/PlatformInterfaces.cs @@ -43,6 +43,8 @@ namespace OpenRA IShader CreateShader(string name); Size WindowSize { get; } + float WindowScale { get; } + event Action OnWindowScaleChanged; void Clear(); void Present(); diff --git a/OpenRA.Game/Graphics/SpriteFont.cs b/OpenRA.Game/Graphics/SpriteFont.cs index 77dda6180c..5a67268cc3 100644 --- a/OpenRA.Game/Graphics/SpriteFont.cs +++ b/OpenRA.Game/Graphics/SpriteFont.cs @@ -29,27 +29,37 @@ namespace OpenRA.Graphics readonly Face face; readonly Cache, GlyphInfo> glyphs; - public SpriteFont(string name, byte[] data, int size, SheetBuilder builder) + float deviceScale; + + public SpriteFont(string name, byte[] data, int size, float scale, SheetBuilder builder) { if (builder.Type != SheetType.BGRA) throw new ArgumentException("The sheet builder must create BGRA sheets.", "builder"); + deviceScale = scale; this.size = size; this.builder = builder; face = new Face(Library, data, 0); - face.SetPixelSizes((uint)size, (uint)size); + face.SetPixelSizes((uint)(size * deviceScale), (uint)(size * deviceScale)); glyphs = new Cache, GlyphInfo>(CreateGlyph, Pair.EqualityComparer); // PERF: Cache these delegates for Measure calls. Func characterWidth = character => glyphs[Pair.New(character, Color.White)].Advance; - lineWidth = line => line.Sum(characterWidth); + lineWidth = line => line.Sum(characterWidth) / deviceScale; PrecacheColor(Color.White, name); PrecacheColor(Color.Red, name); } + public void SetScale(float scale) + { + deviceScale = scale; + face.SetPixelSizes((uint)(size * deviceScale), (uint)(size * deviceScale)); + glyphs.Clear(); + } + void PrecacheColor(Color c, string name) { using (new PerfTimer("PrecacheColor {0} {1}px {2}".F(name, size, c.Name))) @@ -75,9 +85,10 @@ namespace OpenRA.Graphics var g = glyphs[Pair.New(s, c)]; Game.Renderer.RgbaSpriteRenderer.DrawSprite(g.Sprite, new float2( - (int)Math.Round(p.X + g.Offset.X, 0), - p.Y + g.Offset.Y)); - p.X += g.Advance; + (int)Math.Round(p.X * deviceScale + g.Offset.X, 0) / deviceScale, + p.Y + g.Offset.Y / deviceScale), + g.Sprite.Size / deviceScale); + p.X += g.Advance / deviceScale; } } @@ -85,10 +96,10 @@ namespace OpenRA.Graphics { if (offset > 0) { - DrawText(text, location + new float2(-offset, 0), bg); - DrawText(text, location + new float2(offset, 0), bg); - DrawText(text, location + new float2(0, -offset), bg); - DrawText(text, location + new float2(0, offset), bg); + DrawText(text, location + new float2(-offset / deviceScale, 0), bg); + DrawText(text, location + new float2(offset / deviceScale, 0), bg); + DrawText(text, location + new float2(0, -offset / deviceScale), bg); + DrawText(text, location + new float2(0, offset / deviceScale), bg); } DrawText(text, location, fg); diff --git a/OpenRA.Game/Primitives/Cache.cs b/OpenRA.Game/Primitives/Cache.cs index 700991cc73..d162374b80 100644 --- a/OpenRA.Game/Primitives/Cache.cs +++ b/OpenRA.Game/Primitives/Cache.cs @@ -39,6 +39,8 @@ namespace OpenRA.Primitives public bool ContainsKey(T key) { return cache.ContainsKey(key); } public bool TryGetValue(T key, out U value) { return cache.TryGetValue(key, out value); } public int Count { get { return cache.Count; } } + public void Clear() { cache.Clear(); } + public ICollection Keys { get { return cache.Keys; } } public ICollection Values { get { return cache.Values; } } public IEnumerator> GetEnumerator() { return cache.GetEnumerator(); } diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index 35296e475a..fdddfbd0b2 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -86,8 +86,15 @@ namespace OpenRA fontSheetBuilder.Dispose(); fontSheetBuilder = new SheetBuilder(SheetType.BGRA); Fonts = modData.Manifest.Fonts.ToDictionary(x => x.Key, - x => new SpriteFont(x.Value.First, modData.DefaultFileSystem.Open(x.Value.First).ReadAllBytes(), x.Value.Second, fontSheetBuilder)).AsReadOnly(); + x => new SpriteFont(x.Value.First, modData.DefaultFileSystem.Open(x.Value.First).ReadAllBytes(), + x.Value.Second, Device.WindowScale, fontSheetBuilder)).AsReadOnly(); } + + Device.OnWindowScaleChanged += (before, after) => + { + foreach (var f in Fonts) + f.Value.SetScale(after); + }; } public void InitializeDepthBuffer(MapGrid mapGrid) diff --git a/OpenRA.Platforms.Default/Sdl2GraphicsDevice.cs b/OpenRA.Platforms.Default/Sdl2GraphicsDevice.cs index 19812ccd2f..01466c0c79 100644 --- a/OpenRA.Platforms.Default/Sdl2GraphicsDevice.cs +++ b/OpenRA.Platforms.Default/Sdl2GraphicsDevice.cs @@ -21,16 +21,28 @@ namespace OpenRA.Platforms.Default sealed class Sdl2GraphicsDevice : ThreadAffine, IGraphicsDevice { readonly Sdl2Input input; + IntPtr context, window; bool disposed; public Size WindowSize { get; private set; } + public float WindowScale { get; private set; } + + internal Size SurfaceSize { get; private set; } + public event Action OnWindowScaleChanged = (before, after) => { }; + + [DllImport("user32.dll")] + static extern bool SetProcessDPIAware(); public Sdl2GraphicsDevice(Size windowSize, WindowMode windowMode) { Console.WriteLine("Using SDL 2 with OpenGL renderer"); WindowSize = windowSize; + // Disable legacy scaling on Windows + if (Platform.CurrentPlatform == PlatformType.Windows) + SetProcessDPIAware(); + SDL.SDL_Init(SDL.SDL_INIT_NOPARACHUTE | SDL.SDL_INIT_VIDEO); SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_DOUBLEBUFFER, 1); SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_RED_SIZE, 8); @@ -50,8 +62,44 @@ namespace OpenRA.Platforms.Default Console.WriteLine("Using resolution: {0}x{1}", WindowSize.Width, WindowSize.Height); + var windowFlags = SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL.SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI; window = SDL.SDL_CreateWindow("OpenRA", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, - WindowSize.Width, WindowSize.Height, SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL); + WindowSize.Width, WindowSize.Height, windowFlags); + + SurfaceSize = WindowSize; + WindowScale = 1; + + // Enable high resolution rendering for Retina displays + if (Platform.CurrentPlatform == PlatformType.OSX) + { + // OSX defines the window size in "points", with a device-dependent number of pixels per point. + // The window scale is simply the ratio of GL pixels / window points. + int width, height; + SDL.SDL_GL_GetDrawableSize(window, out width, out height); + SurfaceSize = new Size(width, height); + WindowScale = width * 1f / WindowSize.Width; + } + else if (Platform.CurrentPlatform == PlatformType.Windows) + { + float ddpi, hdpi, vdpi; + if (SDL.SDL_GetDisplayDPI(0, out ddpi, out hdpi, out vdpi) == 0) + { + WindowScale = ddpi / 96; + WindowSize = new Size((int)(SurfaceSize.Width / WindowScale), (int)(SurfaceSize.Height / WindowScale)); + } + } + else + { + float scale = 1; + var scaleVariable = Environment.GetEnvironmentVariable("OPENRA_DISPLAY_SCALE"); + if (scaleVariable != null && float.TryParse(scaleVariable, out scale)) + { + WindowScale = scale; + WindowSize = new Size((int)(SurfaceSize.Width / WindowScale), (int)(SurfaceSize.Height / WindowScale)); + } + } + + Console.WriteLine("Using window scale {0:F2}", WindowScale); if (Game.Settings.Game.LockMouseWindow) GrabWindowMouseFocus(); @@ -93,6 +141,29 @@ namespace OpenRA.Platforms.Default VerifyThreadAffinity(); try { + // 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) + { + var scaledData = new byte[4 * data.Length]; + for (var y = 0; y < size.Height * 4; y += 4) + { + for (var x = 0; x < size.Width * 4; x += 4) + { + var a = 4 * (y * size.Width + x); + var b = 4 * ((y + 1) * size.Width + x); + for (var i = 0; i < 4; i++) + { + scaledData[2 * a + i] = scaledData[2 * a + 4 + i] = data[a + i]; + scaledData[2 * b + i] = scaledData[2 * b + 4 + i] = data[b + i]; + } + } + } + + size = new Size(2 * size.Width, 2 * size.Height); + data = scaledData; + } + return new SDL2HardwareCursor(size, data, hotspot); } catch (Exception ex) @@ -114,6 +185,26 @@ namespace OpenRA.Platforms.Default } } + internal void WindowSizeChanged() + { + // The ratio between pixels and points can change when moving between displays in OSX + // We need to recalculate our scale to account for the potential change in the actual rendered area + if (Platform.CurrentPlatform == PlatformType.OSX) + { + int width, height; + SDL.SDL_GL_GetDrawableSize(window, out width, out height); + + if (width != SurfaceSize.Width || height != SurfaceSize.Height) + { + var oldScale = WindowScale; + SurfaceSize = new Size(width, height); + WindowScale = width * 1f / WindowSize.Width; + + OnWindowScaleChanged(oldScale, WindowScale); + } + } + } + sealed class SDL2HardwareCursor : IHardwareCursor { public IntPtr Cursor { get; private set; } @@ -315,7 +406,16 @@ namespace OpenRA.Platforms.Default if (height < 0) height = 0; - OpenGL.glScissor(left, WindowSize.Height - (top + height), width, height); + var bottom = WindowSize.Height - (top + height); + if (WindowSize != SurfaceSize) + { + left = (int)Math.Round(WindowScale * left); + bottom = (int)Math.Round(WindowScale * bottom); + width = (int)Math.Round(WindowScale * width); + height = (int)Math.Round(WindowScale * height); + } + + OpenGL.glScissor(left, bottom, width, height); OpenGL.CheckGLError(); OpenGL.glEnable(OpenGL.GL_SCISSOR_TEST); OpenGL.CheckGLError(); @@ -330,7 +430,7 @@ namespace OpenRA.Platforms.Default public Bitmap TakeScreenshot() { - var rect = new Rectangle(Point.Empty, WindowSize); + var rect = new Rectangle(Point.Empty, SurfaceSize); var bitmap = new Bitmap(rect.Width, rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); var data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); @@ -362,7 +462,7 @@ namespace OpenRA.Platforms.Default public void PumpInput(IInputHandler inputHandler) { VerifyThreadAffinity(); - input.PumpInput(inputHandler); + input.PumpInput(this, inputHandler); } public string GetClipboardText() diff --git a/OpenRA.Platforms.Default/Sdl2Input.cs b/OpenRA.Platforms.Default/Sdl2Input.cs index 012685892d..f69b5d76a8 100644 --- a/OpenRA.Platforms.Default/Sdl2Input.cs +++ b/OpenRA.Platforms.Default/Sdl2Input.cs @@ -40,7 +40,17 @@ namespace OpenRA.Platforms.Default | ((raw & (int)SDL.SDL_Keymod.KMOD_SHIFT) != 0 ? Modifiers.Shift : 0); } - public void PumpInput(IInputHandler inputHandler) + int2 EventPosition(Sdl2GraphicsDevice device, int x, int y) + { + // 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)); + + return new int2(x, y); + } + + public void PumpInput(Sdl2GraphicsDevice device, IInputHandler inputHandler) { var mods = MakeModifiers((int)SDL.SDL_GetModState()); var scrollDelta = 0; @@ -67,6 +77,11 @@ namespace OpenRA.Platforms.Default case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED: Game.HasInputFocus = true; break; + + // Triggered when moving between displays with different DPI settings + case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED: + device.WindowSizeChanged(); + break; } break; @@ -83,8 +98,7 @@ namespace OpenRA.Platforms.Default var button = MakeButton(e.button.button); lastButtonBits |= button; - var pos = new int2(e.button.x, e.button.y); - + var pos = EventPosition(device, e.button.x, e.button.y); inputHandler.OnMouseInput(new MouseInput( MouseInputEvent.Down, button, scrollDelta, pos, mods, MultiTapDetection.DetectFromMouse(e.button.button, pos))); @@ -103,7 +117,7 @@ namespace OpenRA.Platforms.Default var button = MakeButton(e.button.button); lastButtonBits &= ~button; - var pos = new int2(e.button.x, e.button.y); + var pos = EventPosition(device, e.button.x, e.button.y); inputHandler.OnMouseInput(new MouseInput( MouseInputEvent.Up, button, scrollDelta, pos, mods, MultiTapDetection.InfoFromMouse(e.button.button))); @@ -113,9 +127,10 @@ namespace OpenRA.Platforms.Default case SDL.SDL_EventType.SDL_MOUSEMOTION: { + var pos = EventPosition(device, e.motion.x, e.motion.y); pendingMotion = new MouseInput( MouseInputEvent.Move, lastButtonBits, scrollDelta, - new int2(e.motion.x, e.motion.y), mods, 0); + pos, mods, 0); break; } diff --git a/packaging/osx/buildpackage.sh b/packaging/osx/buildpackage.sh index 465dad5135..64430e8b10 100755 --- a/packaging/osx/buildpackage.sh +++ b/packaging/osx/buildpackage.sh @@ -1,7 +1,7 @@ #!/bin/bash # OpenRA packaging script for Mac OSX -LAUNCHER_TAG="osx-launcher-20160824" +LAUNCHER_TAG="osx-launcher-20161223" if [ $# -ne "3" ]; then echo "Usage: `basename $0` tag files-dir outputdir" diff --git a/thirdparty/fetch-thirdparty-deps.ps1 b/thirdparty/fetch-thirdparty-deps.ps1 index d06a114070..3dad43438a 100644 --- a/thirdparty/fetch-thirdparty-deps.ps1 +++ b/thirdparty/fetch-thirdparty-deps.ps1 @@ -130,7 +130,7 @@ if (!(Test-Path "SDL2-CS.dll")) { echo "Fetching SDL2-CS from GitHub." $target = Join-Path $pwd.ToString() "SDL2-CS.dll" - (New-Object System.Net.WebClient).DownloadFile("https://github.com/OpenRA/SDL2-CS/releases/download/20151227/SDL2-CS.dll", $target) + (New-Object System.Net.WebClient).DownloadFile("https://github.com/OpenRA/SDL2-CS/releases/download/20161223/SDL2-CS.dll", $target) } if (!(Test-Path "OpenAL-CS.dll")) diff --git a/thirdparty/fetch-thirdparty-deps.sh b/thirdparty/fetch-thirdparty-deps.sh index d5735063ab..ca4414b2d9 100755 --- a/thirdparty/fetch-thirdparty-deps.sh +++ b/thirdparty/fetch-thirdparty-deps.sh @@ -100,8 +100,8 @@ fi if [ ! -f SDL2-CS.dll -o ! -f SDL2-CS.dll.config ]; then echo "Fetching SDL2-CS from GitHub." - curl -s -L -O https://github.com/OpenRA/SDL2-CS/releases/download/20151227/SDL2-CS.dll - curl -s -L -O https://github.com/OpenRA/SDL2-CS/releases/download/20151227/SDL2-CS.dll.config + curl -s -L -O https://github.com/OpenRA/SDL2-CS/releases/download/20161223/SDL2-CS.dll + curl -s -L -O https://github.com/OpenRA/SDL2-CS/releases/download/20161223/SDL2-CS.dll.config fi if [ ! -f OpenAL-CS.dll -o ! -f OpenAL-CS.dll.config ]; then