Some work towards multi-monitor support in Clyde.

Most of this was me experimenting with GLFW, but I figured I'd still commit it.
This commit is contained in:
Pieter-Jan Briers
2021-03-28 21:23:24 +02:00
parent ef22842b90
commit ddc91d05ec
5 changed files with 232 additions and 12 deletions

View File

@@ -0,0 +1,44 @@
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
public sealed class LsMonitorCommand : IConsoleCommand
{
public string Command => "lsmonitor";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
foreach (var monitor in clyde.EnumerateMonitors())
{
shell.WriteLine(
$"[{monitor.Id}] {monitor.Name}: {monitor.Size.X}x{monitor.Size.Y}@{monitor.RefreshRate}Hz");
}
}
}
[UsedImplicitly]
public sealed class SetMonitorCommand : IConsoleCommand
{
public string Command => "setmonitor";
public string Description => "";
public string Help => "Usage: setmonitor <id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var id = int.Parse(args[0]);
var monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
clyde.SetWindowMonitor(monitor);
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
@@ -14,6 +15,7 @@ using Robust.Shared;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static Robust.Client.Utility.LiterallyJustMessageBox;
@@ -46,6 +48,7 @@ namespace Robust.Client.Graphics.Clyde
{
// Keep delegates around to prevent GC issues.
private GLFWCallbacks.ErrorCallback _errorCallback = default!;
private GLFWCallbacks.MonitorCallback _monitorCallback = default!;
private GLFWCallbacks.CharCallback _charCallback = default!;
private GLFWCallbacks.CursorPosCallback _cursorPosCallback = default!;
private GLFWCallbacks.KeyCallback _keyCallback = default!;
@@ -73,6 +76,10 @@ namespace Robust.Client.Graphics.Clyde
private Vector2 _lastMousePos;
// Can't use ClydeHandle because it's 64 bit.
private int _nextWindowId = 1;
private readonly Dictionary<int, MonitorReg> _monitors = new();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
@@ -156,11 +163,56 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
InitMonitors();
InitCursors();
return InitWindow();
}
private void InitMonitors()
{
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
SetupMonitor(monitors[i]);
}
}
private void SetupMonitor(Monitor* monitor)
{
var handle = _nextWindowId++;
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, "GLFW window already has user pointer??");
var name = GLFW.GetMonitorName(monitor);
var videoMode = GLFW.GetVideoMode(monitor);
var impl = new ClydeMonitorImpl(handle, name, (videoMode->Width, videoMode->Height), videoMode->RefreshRate);
GLFW.SetMonitorUserPointer(monitor, (void*) handle);
_monitors[handle] = new MonitorReg
{
Id = handle,
Impl = impl,
Monitor = monitor
};
}
private void DestroyMonitor(Monitor* monitor)
{
var ptr = GLFW.GetMonitorUserPointer(monitor);
if (ptr == null)
{
var name = GLFW.GetMonitorName(monitor);
Logger.WarningS("clyde.win", $"Monitor '{name}' had no user pointer set??");
return;
}
_monitors.Remove((int) ptr);
GLFW.SetMonitorUserPointer(monitor, null);
}
private bool InitWindow()
{
var width = ConfigurationManager.GetCVar(CVars.DisplayWidth);
@@ -170,10 +222,12 @@ namespace Robust.Client.Graphics.Clyde
if (WindowMode == WindowMode.Fullscreen)
{
monitor = GLFW.GetPrimaryMonitor();
monitor = GLFW.GetMonitors()[1];
var mode = GLFW.GetVideoMode(monitor);
width = mode->Width;
height = mode->Height;
GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate);
}
#if DEBUG
@@ -184,11 +238,14 @@ namespace Robust.Client.Graphics.Clyde
var renderer = (Renderer) ConfigurationManager.GetCVar<int>(CVars.DisplayRenderer);
Span<Renderer> renderers = (renderer == Renderer.Default) ? stackalloc Renderer[] {
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
} : stackalloc Renderer[] {renderer};
Span<Renderer> renderers = (renderer == Renderer.Default)
? stackalloc Renderer[]
{
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
}
: stackalloc Renderer[] {renderer};
foreach (Renderer r in renderers)
{
@@ -201,6 +258,7 @@ namespace Robust.Client.Graphics.Clyde
_isCore = renderer == Renderer.OpenGL33;
break;
}
// Window failed to init due to error.
// Try not to treat the error code seriously.
var code = GLFW.GetError(out string desc);
@@ -214,9 +272,9 @@ namespace Robust.Client.Graphics.Clyde
var code = GLFW.GetError(out string desc);
var errorContent = "Failed to create the game window. " +
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
MessageBoxW(null,
errorContent,
@@ -309,6 +367,7 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
}
_glfwWindow = GLFW.CreateWindow(width, height, string.Empty, monitor, null);
}
}
@@ -401,6 +460,25 @@ namespace Robust.Client.Graphics.Clyde
Logger.ErrorS("clyde.win.glfw", "GLFW Error: [{0}] {1}", code, description);
}
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
{
try
{
if (state == ConnectedState.Connected)
{
SetupMonitor(monitor);
}
else
{
DestroyMonitor(monitor);
}
}
catch (Exception e)
{
CatchCallbackException(e);
}
}
private void OnGlfwChar(Window* window, uint codepoint)
{
try
@@ -576,6 +654,7 @@ namespace Robust.Client.Graphics.Clyde
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
_monitorCallback = OnGlfwMonitor;
_charCallback = OnGlfwChar;
_cursorPosCallback = OnGlfwCursorPos;
_keyCallback = OnGlfwKey;
@@ -598,6 +677,19 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetWindowTitle(_glfwWindow, title);
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
var monitorImpl = (ClydeMonitorImpl) monitor;
var reg = _monitors[monitorImpl.Id];
GLFW.SetWindowMonitor(
_glfwWindow,
reg.Monitor,
0, 0,
monitorImpl.Size.X, monitorImpl.Size.Y,
monitorImpl.RefreshRate);
}
public void RequestWindowAttention()
{
GLFW.RequestWindowAttention(_glfwWindow);
@@ -662,18 +754,40 @@ namespace Robust.Client.Graphics.Clyde
GLFW.GetWindowPos(_glfwWindow, out var x, out var y);
_prevWindowPos = (x, y);
var monitor = GLFW.GetPrimaryMonitor();
var monitor = MonitorForWindow(_glfwWindow);
var mode = GLFW.GetVideoMode(monitor);
GLFW.SetWindowMonitor(_glfwWindow, GLFW.GetPrimaryMonitor(), 0, 0, mode->Width, mode->Height,
GLFW.SetWindowMonitor(_glfwWindow, monitor, 0, 0, mode->Width, mode->Height,
mode->RefreshRate);
}
else
{
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X, _prevWindowSize.Y, 0);
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X,
_prevWindowSize.Y, 0);
}
}
// glfwGetWindowMonitor only works for fullscreen windows.
// Picks the monitor with the top-left corner of the window.
private Monitor* MonitorForWindow(Window* window)
{
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
var monitor = monitors[i];
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
var videoMode = GLFW.GetVideoMode(monitor);
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
if (box.Contains(winPosX, winPosY))
return monitor;
}
// Fallback
return GLFW.GetPrimaryMonitor();
}
string IClipboardManager.GetText()
{
return GLFW.GetClipboardString(_glfwWindow);
@@ -684,6 +798,11 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetClipboardString(_glfwWindow, text);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
return _monitors.Values.Select(c => c.Impl);
}
// We can't let exceptions unwind into GLFW, as that can cause the CLR to crash.
// And it probably messes up GLFW too.
// So all the callbacks are passed to this method.
@@ -697,5 +816,28 @@ namespace Robust.Client.Graphics.Clyde
_glfwExceptionList.Add(e);
}
private sealed class MonitorReg
{
public int Id;
public Monitor* Monitor;
public ClydeMonitorImpl Impl = default!;
}
private sealed class ClydeMonitorImpl : IClydeMonitor
{
public ClydeMonitorImpl(int id, string name, Vector2i size, int refreshRate)
{
Id = id;
Name = name;
Size = size;
RefreshRate = refreshRate;
}
public int Id { get; }
public string Name { get; }
public Vector2i Size { get; }
public int RefreshRate { get; }
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using Robust.Client.Audio;
@@ -70,6 +71,11 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
// Nada.
}
public void RequestWindowAttention()
{
// Nada.
@@ -169,6 +175,12 @@ namespace Robust.Client.Graphics.Clyde
return new Viewport();
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
// TODO: Actually return something.
yield break;
}
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
{
return default;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Robust.Shared.Maths;
@@ -21,6 +22,7 @@ namespace Robust.Client.Graphics
Vector2 DefaultWindowScale { get; }
void SetWindowTitle(string title);
void SetWindowMonitor(IClydeMonitor monitor);
/// <summary>
/// This is the magic method to make the game window ping you in the task bar.
@@ -106,6 +108,8 @@ namespace Robust.Client.Graphics
}
IClydeViewport CreateViewport(Vector2i size, string? name = null);
IEnumerable<IClydeMonitor> EnumerateMonitors();
}
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
/// <summary>
/// Represents a connected monitor on the user's system.
/// </summary>
public interface IClydeMonitor
{
/// <summary>
/// This ID is not consistent between startups of the game.
/// </summary>
int Id { get; }
string Name { get; }
Vector2i Size { get; }
int RefreshRate { get; }
}
}