* Start converting SDL2 backend to SDL3.

Game starts, but a lot of stuff is broken. Oh well.

* Fix text input

SDL3 changed the API somewhat, for the better. Changes all over UI/Clyde/SDL3 layer.

* Fix mouse buttons being broken

* Remove records from SDL3 WSI

The fact that this shaved 2-3% off Robust.Client.dll is mindboggling. Records are so bad.

* Set Windows/X11 native window properties

* Fix window resize events getting wrong size

oops

* Remove "using static" from SDL3 WSI

Seriously seems to hurt IDE performance, oh well.

* Apparently I never called CheckThreadApartment().

* Add STAThreadAttribute to sandbox

Necessary for content start

* Set window title on creation properly.

* Load window icons

* Fix GLFW NoTitleBar style handling

Yeah this PR is supposed to be about SDL3, so what?

* Implement more window creation settings in SDL3

Mostly the ones that need a lot of platform-specific stuff to work.

* Make fullscreen work properly in SDL3.

* File dialogs with SDL3

Removes need for swnfd.

* Fix some TODOs

* Fix WebView build
This commit is contained in:
Pieter-Jan Briers
2025-01-03 18:42:57 +01:00
committed by GitHub
parent e47ba0faea
commit 87a5745519
35 changed files with 10240 additions and 10663 deletions

View File

@@ -484,27 +484,27 @@ namespace Robust.Client.WebView.Cef
public void FocusEntered()
{
if (_textInputActive)
_clyde.TextInputStart();
Owner.Root?.Window?.TextInputStart();
}
public void FocusExited()
{
if (_textInputActive)
_clyde.TextInputStop();
Owner.Root?.Window?.TextInputStop();
}
public void TextInputStart()
{
_textInputActive = true;
if (Owner.HasKeyboardFocus())
_clyde.TextInputStart();
Owner.Root?.Window?.TextInputStart();
}
public void TextInputStop()
{
_textInputActive = false;
if (Owner.HasKeyboardFocus())
_clyde.TextInputStop();
Owner.Root?.Window?.TextInputStop();
}
private sealed class LiveData

View File

@@ -23,6 +23,7 @@ namespace Robust.Client
private Thread? _gameThread;
private ISawmill _logger = default!;
[STAThread]
public static void Main(string[] args)
{
Start(args, new GameControllerOptions());

View File

@@ -109,8 +109,8 @@ namespace Robust.Client.Graphics.Clyde
case "glfw":
winImpl = new GlfwWindowingImpl(this, _deps);
break;
case "sdl2":
winImpl = new Sdl2WindowingImpl(this, _deps);
case "sdl3":
winImpl = new Sdl3WindowingImpl(this, _deps);
break;
default:
_logManager.GetSawmill("clyde.win").Log(
@@ -467,26 +467,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.RunOnWindowThread(a);
}
public void TextInputSetRect(UIBox2i rect)
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputSetRect(rect);
}
public void TextInputStart()
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputStart();
}
public void TextInputStop()
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputStop();
}
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
private abstract class WindowReg
{
@@ -590,6 +571,27 @@ namespace Robust.Client.Graphics.Clyde
remove => Reg.Resized -= value;
}
public void TextInputSetRect(UIBox2i rect, int cursor)
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputSetRect(Reg, rect, cursor);
}
public void TextInputStart()
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputStart(Reg);
}
public void TextInputStop()
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputStop(Reg);
}
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
}

View File

@@ -7,6 +7,7 @@ using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
@@ -284,6 +285,8 @@ namespace Robust.Client.Graphics.Clyde
action();
}
public IFileDialogManager? FileDialogImpl => null;
private sealed class DummyCursor : ICursor
{
public void Dispose()
@@ -531,6 +534,21 @@ namespace Robust.Client.Graphics.Clyde
public event Action<WindowDestroyedEventArgs>? Destroyed;
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
public void TextInputSetRect(UIBox2i rect, int cursor)
{
// Nop.
}
public void TextInputStart()
{
// Nop.
}
public void TextInputStop()
{
// Nop.
}
public void MaximizeOnMonitor(IClydeMonitor monitor)
{
}

View File

@@ -96,7 +96,8 @@ namespace Robust.Client.Graphics.Clyde
private void ProcessEventChar(EventChar ev)
{
if (!_textInputActive)
var windowReg = FindWindow(ev.Window);
if (windowReg is not { TextInputActive: true })
return;
_clyde.SendText(new TextEnteredEventArgs(new Rune(ev.CodePoint).ToString()));

View File

@@ -468,6 +468,8 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintInt.AlphaBits, 8);
GLFW.WindowHint(WindowHintInt.StencilBits, 8);
GLFW.WindowHint(WindowHintBool.Decorated, (parameters.Styles & OSWindowStyles.NoTitleBar) == 0);
var window = GLFW.CreateWindow(
parameters.Width, parameters.Height,
parameters.Title,
@@ -485,23 +487,12 @@ namespace Robust.Client.Graphics.Clyde
GLFW.MaximizeWindow(window);
}
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
{
GLFW.WindowHint(WindowHintBool.Decorated, false);
}
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
{
if (OperatingSystem.IsWindows())
{
var hWnd = (HWND) GLFW.GetWin32Window(window);
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWL.GWL_STYLE,
// Cast to long here to work around a bug in rider with nint bitwise operators.
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
WsiShared.SetWindowStyleNoTitleOptionsWindows(hWnd);
}
else if (OperatingSystem.IsLinux())
{
@@ -509,23 +500,7 @@ namespace Robust.Client.Graphics.Clyde
{
var x11Window = (X11Window)GLFW.GetX11Window(window);
var x11Display = (Display*) GLFW.GetX11Display(window);
DebugTools.Assert(x11Window != X11Window.NULL);
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
DebugTools.Assert(newPropVal != Atom.NULL);
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
#pragma warning disable CA1806
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
Xlib.XChangeProperty(x11Display, x11Window,
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
(byte*)&newPropVal, 1);
#pragma warning restore CA1806
Marshal.FreeCoTaskMem(newPropValString);
Marshal.FreeCoTaskMem(propNameString);
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
}
catch (EntryPointNotFoundException)
{
@@ -643,16 +618,16 @@ namespace Robust.Client.Graphics.Clyde
return reg;
}
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
private GlfwWindowReg? FindWindow(nint window) => FindWindow((Window*) window);
private WindowReg? FindWindow(Window* window)
private GlfwWindowReg? FindWindow(Window* window)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (GlfwWindowReg) windowReg;
if (glfwReg.GlfwWindow == window)
{
return windowReg;
return glfwReg;
}
}
@@ -739,23 +714,23 @@ namespace Robust.Client.Graphics.Clyde
return (void*) GLFW.GetProcAddress(procName);
}
public void TextInputSetRect(UIBox2i rect)
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
// Not supported on GLFW.
}
public void TextInputStart()
public void TextInputStart(WindowReg reg)
{
// Not properly supported on GLFW.
_textInputActive = true;
((GlfwWindowReg)reg).TextInputActive = true;
}
public void TextInputStop()
public void TextInputStop(WindowReg reg)
{
// Not properly supported on GLFW.
_textInputActive = false;
((GlfwWindowReg)reg).TextInputActive = false;
}
private void CheckWindowDisposed(WindowReg reg)
@@ -770,6 +745,10 @@ namespace Robust.Client.Graphics.Clyde
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
// While GLFW does not provide proper IME APIs, we can at least emulate SDL3's StartTextInput() system.
// This will ensure some level of consistency between the backends.
public bool TextInputActive;
}
}
}

View File

@@ -25,10 +25,6 @@ namespace Robust.Client.Graphics.Clyde
private bool _glfwInitialized;
private bool _win32Experience;
// While GLFW does not provide proper IME APIs, we can at least emulate SDL2's StartTextInput() system.
// This will ensure some level of consistency between the backends.
private bool _textInputActive;
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;

View File

@@ -66,9 +66,9 @@ namespace Robust.Client.Graphics.Clyde
void RunOnWindowThread(Action a);
// IME
void TextInputSetRect(UIBox2i rect);
void TextInputStart();
void TextInputStop();
void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor);
void TextInputStart(WindowReg reg);
void TextInputStop(WindowReg reg);
string GetDescription();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
/* SDL3-CS - C# Bindings for SDL3
*
* Copyright (c) 2024 Colin Jackson
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Colin "cryy22" Jackson <c@cryy22.art>
*
*/

View File

@@ -0,0 +1,52 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SDL3;
public static partial class SDL
{
// Extensions to SDL3-CS that aren't part of the main library.
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
public const byte SDL_BUTTON_LEFT = 1;
public const byte SDL_BUTTON_MIDDLE = 2;
public const byte SDL_BUTTON_RIGHT = 3;
public const byte SDL_BUTTON_X1 = 4;
public const byte SDL_BUTTON_X2 = 5;
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
public const int SDL_FILEDIALOG_OPENFILE = 0;
public const int SDL_FILEDIALOG_SAVEFILE = 1;
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,224 +0,0 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Robust.Client.Input;
using Robust.Shared;
using static SDL2.SDL;
using static SDL2.SDL.SDL_Scancode;
using Key = Robust.Client.Input.Keyboard.Key;
using Button = Robust.Client.Input.Mouse.Button;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
// Indices are values of SDL_Scancode
private static readonly Key[] KeyMap;
private static readonly FrozenDictionary<Key, SDL_Scancode> KeyMapReverse;
private static readonly Button[] MouseButtonMap;
// TODO: to avoid having to ask the windowing thread, key names are cached.
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
private void ReloadKeyMap()
{
// This may be ran concurrently from the windowing thread.
lock (_printableKeyNameMap)
{
_printableKeyNameMap.Clear();
// List of mappable keys from SDL2's source appears to be:
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
// (the actual logic is more nuanced, but it appears to match the above)
// Comes out to these two ranges:
for (var k = SDL_SCANCODE_A; k <= SDL_SCANCODE_0; k++)
{
CacheKey(k);
}
for (var k = SDL_SCANCODE_MINUS; k <= SDL_SCANCODE_SLASH; k++)
{
CacheKey(k);
}
void CacheKey(SDL_Scancode scancode)
{
var rKey = ConvertSdl2Scancode(scancode);
if (rKey == Key.Unknown)
return;
var name = SDL_GetKeyName(SDL_GetKeyFromScancode(scancode));
if (!string.IsNullOrEmpty(name))
_printableKeyNameMap.Add(rKey, name);
}
}
}
public string? KeyGetName(Key key)
{
lock (_printableKeyNameMap)
{
if (_printableKeyNameMap.TryGetValue(key, out var name))
return name;
return null;
}
}
internal static Key ConvertSdl2Scancode(SDL_Scancode scancode)
{
return KeyMap[(int) scancode];
}
public static Button ConvertSdl2Button(int button)
{
return MouseButtonMap[button];
}
static Sdl2WindowingImpl()
{
MouseButtonMap = new Button[6];
MouseButtonMap[SDL_BUTTON_LEFT] = Button.Left;
MouseButtonMap[SDL_BUTTON_RIGHT] = Button.Right;
MouseButtonMap[SDL_BUTTON_MIDDLE] = Button.Middle;
MouseButtonMap[SDL_BUTTON_X1] = Button.Button4;
MouseButtonMap[SDL_BUTTON_X2] = Button.Button5;
KeyMap = new Key[(int) SDL_NUM_SCANCODES];
MapKey(SDL_SCANCODE_A, Key.A);
MapKey(SDL_SCANCODE_B, Key.B);
MapKey(SDL_SCANCODE_C, Key.C);
MapKey(SDL_SCANCODE_D, Key.D);
MapKey(SDL_SCANCODE_E, Key.E);
MapKey(SDL_SCANCODE_F, Key.F);
MapKey(SDL_SCANCODE_G, Key.G);
MapKey(SDL_SCANCODE_H, Key.H);
MapKey(SDL_SCANCODE_I, Key.I);
MapKey(SDL_SCANCODE_J, Key.J);
MapKey(SDL_SCANCODE_K, Key.K);
MapKey(SDL_SCANCODE_L, Key.L);
MapKey(SDL_SCANCODE_M, Key.M);
MapKey(SDL_SCANCODE_N, Key.N);
MapKey(SDL_SCANCODE_O, Key.O);
MapKey(SDL_SCANCODE_P, Key.P);
MapKey(SDL_SCANCODE_Q, Key.Q);
MapKey(SDL_SCANCODE_R, Key.R);
MapKey(SDL_SCANCODE_S, Key.S);
MapKey(SDL_SCANCODE_T, Key.T);
MapKey(SDL_SCANCODE_U, Key.U);
MapKey(SDL_SCANCODE_V, Key.V);
MapKey(SDL_SCANCODE_W, Key.W);
MapKey(SDL_SCANCODE_X, Key.X);
MapKey(SDL_SCANCODE_Y, Key.Y);
MapKey(SDL_SCANCODE_Z, Key.Z);
MapKey(SDL_SCANCODE_0, Key.Num0);
MapKey(SDL_SCANCODE_1, Key.Num1);
MapKey(SDL_SCANCODE_2, Key.Num2);
MapKey(SDL_SCANCODE_3, Key.Num3);
MapKey(SDL_SCANCODE_4, Key.Num4);
MapKey(SDL_SCANCODE_5, Key.Num5);
MapKey(SDL_SCANCODE_6, Key.Num6);
MapKey(SDL_SCANCODE_7, Key.Num7);
MapKey(SDL_SCANCODE_8, Key.Num8);
MapKey(SDL_SCANCODE_9, Key.Num9);
MapKey(SDL_SCANCODE_KP_0, Key.NumpadNum0);
MapKey(SDL_SCANCODE_KP_1, Key.NumpadNum1);
MapKey(SDL_SCANCODE_KP_2, Key.NumpadNum2);
MapKey(SDL_SCANCODE_KP_3, Key.NumpadNum3);
MapKey(SDL_SCANCODE_KP_4, Key.NumpadNum4);
MapKey(SDL_SCANCODE_KP_5, Key.NumpadNum5);
MapKey(SDL_SCANCODE_KP_6, Key.NumpadNum6);
MapKey(SDL_SCANCODE_KP_7, Key.NumpadNum7);
MapKey(SDL_SCANCODE_KP_8, Key.NumpadNum8);
MapKey(SDL_SCANCODE_KP_9, Key.NumpadNum9);
MapKey(SDL_SCANCODE_ESCAPE, Key.Escape);
MapKey(SDL_SCANCODE_LCTRL, Key.Control);
MapKey(SDL_SCANCODE_RCTRL, Key.Control);
MapKey(SDL_SCANCODE_RSHIFT, Key.Shift);
MapKey(SDL_SCANCODE_LSHIFT, Key.Shift);
MapKey(SDL_SCANCODE_LALT, Key.Alt);
MapKey(SDL_SCANCODE_RALT, Key.Alt);
MapKey(SDL_SCANCODE_LGUI, Key.LSystem);
MapKey(SDL_SCANCODE_RGUI, Key.RSystem);
MapKey(SDL_SCANCODE_MENU, Key.Menu);
MapKey(SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
MapKey(SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
MapKey(SDL_SCANCODE_SEMICOLON, Key.SemiColon);
MapKey(SDL_SCANCODE_COMMA, Key.Comma);
MapKey(SDL_SCANCODE_PERIOD, Key.Period);
MapKey(SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
MapKey(SDL_SCANCODE_SLASH, Key.Slash);
MapKey(SDL_SCANCODE_BACKSLASH, Key.BackSlash);
MapKey(SDL_SCANCODE_GRAVE, Key.Tilde);
MapKey(SDL_SCANCODE_EQUALS, Key.Equal);
MapKey(SDL_SCANCODE_SPACE, Key.Space);
MapKey(SDL_SCANCODE_RETURN, Key.Return);
MapKey(SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
MapKey(SDL_SCANCODE_BACKSPACE, Key.BackSpace);
MapKey(SDL_SCANCODE_TAB, Key.Tab);
MapKey(SDL_SCANCODE_PAGEUP, Key.PageUp);
MapKey(SDL_SCANCODE_PAGEDOWN, Key.PageDown);
MapKey(SDL_SCANCODE_END, Key.End);
MapKey(SDL_SCANCODE_HOME, Key.Home);
MapKey(SDL_SCANCODE_INSERT, Key.Insert);
MapKey(SDL_SCANCODE_DELETE, Key.Delete);
MapKey(SDL_SCANCODE_MINUS, Key.Minus);
MapKey(SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
MapKey(SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
MapKey(SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
MapKey(SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
MapKey(SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
MapKey(SDL_SCANCODE_LEFT, Key.Left);
MapKey(SDL_SCANCODE_RIGHT, Key.Right);
MapKey(SDL_SCANCODE_UP, Key.Up);
MapKey(SDL_SCANCODE_DOWN, Key.Down);
MapKey(SDL_SCANCODE_F1, Key.F1);
MapKey(SDL_SCANCODE_F2, Key.F2);
MapKey(SDL_SCANCODE_F3, Key.F3);
MapKey(SDL_SCANCODE_F4, Key.F4);
MapKey(SDL_SCANCODE_F5, Key.F5);
MapKey(SDL_SCANCODE_F6, Key.F6);
MapKey(SDL_SCANCODE_F7, Key.F7);
MapKey(SDL_SCANCODE_F8, Key.F8);
MapKey(SDL_SCANCODE_F9, Key.F9);
MapKey(SDL_SCANCODE_F10, Key.F10);
MapKey(SDL_SCANCODE_F11, Key.F11);
MapKey(SDL_SCANCODE_F12, Key.F12);
MapKey(SDL_SCANCODE_F13, Key.F13);
MapKey(SDL_SCANCODE_F14, Key.F14);
MapKey(SDL_SCANCODE_F15, Key.F15);
MapKey(SDL_SCANCODE_F16, Key.F16);
MapKey(SDL_SCANCODE_F17, Key.F17);
MapKey(SDL_SCANCODE_F18, Key.F18);
MapKey(SDL_SCANCODE_F19, Key.F19);
MapKey(SDL_SCANCODE_F20, Key.F20);
MapKey(SDL_SCANCODE_F21, Key.F21);
MapKey(SDL_SCANCODE_F22, Key.F22);
MapKey(SDL_SCANCODE_F23, Key.F23);
MapKey(SDL_SCANCODE_F24, Key.F24);
MapKey(SDL_SCANCODE_PAUSE, Key.Pause);
var keyMapReverse = new Dictionary<Key, SDL_Scancode>();
for (var code = 0; code < KeyMap.Length; code++)
{
var key = KeyMap[code];
if (key != Key.Unknown)
keyMapReverse[key] = (SDL_Scancode) code;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void MapKey(SDL_Scancode code, Key key)
{
KeyMap[(int)code] = key;
}
}
}
}

View File

@@ -1,133 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL2;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
// NOTE: SDL2 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
// Can't use ClydeHandle because it's not thread safe to allocate.
private int _nextMonitorId = 1;
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
private readonly Dictionary<int, Sdl2MonitorReg> _monitors = new();
private void InitMonitors()
{
var numDisplays = SDL.SDL_GetNumVideoDisplays();
for (var i = 0; i < numDisplays; i++)
{
// SDL.SDL_GetDisplayDPI(i, out var ddpi, out var hdpi, out var vdpi);
// _sawmill.Info($"[{i}] {ddpi} {hdpi} {vdpi}");
WinThreadSetupMonitor(i);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void WinThreadSetupMonitor(int displayIdx)
{
var id = _nextMonitorId++;
var name = SDL.SDL_GetDisplayName(displayIdx);
var modeCount = SDL.SDL_GetNumDisplayModes(displayIdx);
SDL.SDL_GetCurrentDisplayMode(displayIdx, out var curMode);
var modes = new VideoMode[modeCount];
for (var i = 0; i < modes.Length; i++)
{
SDL.SDL_GetDisplayMode(displayIdx, i, out var mode);
modes[i] = ConvertVideoMode(mode);
}
_winThreadMonitors.Add(id, new WinThreadMonitorReg { Id = id, DisplayIdx = displayIdx });
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(curMode), modes));
if (displayIdx == 0)
_clyde._primaryMonitorId = id;
}
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
{
return new()
{
Width = (ushort)mode.w,
Height = (ushort)mode.h,
RefreshRate = (ushort)mode.refresh_rate,
// TODO: set bits count based on format (I'm lazy)
RedBits = 8,
GreenBits = 8,
BlueBits = 8,
};
}
private void ProcessSetupMonitor(EventMonitorSetup ev)
{
var impl = new MonitorHandle(
ev.Id,
ev.Name,
(ev.CurrentMode.Width, ev.CurrentMode.Height),
ev.CurrentMode.RefreshRate,
ev.AllModes);
_clyde._monitorHandles.Add(ev.Id, impl);
_monitors[ev.Id] = new Sdl2MonitorReg
{
Id = ev.Id,
Handle = impl
};
}
private void WinThreadDestroyMonitor(int displayIdx)
{
var monitorId = 0;
foreach (var (id, monitorReg) in _winThreadMonitors)
{
if (monitorReg.DisplayIdx == displayIdx)
{
monitorId = id;
break;
}
}
if (monitorId == 0)
return;
// So SDL2 doesn't have a very nice indexing system for monitors like GLFW does.
// This means that, when a monitor is disconnected, all monitors *after* it get shifted down one slot.
// Now, this happens *after* the event is fired, to make matters worse.
// So we're basically trying to match unspecified SDL2 internals here. Great.
_winThreadMonitors.Remove(monitorId);
foreach (var (_, reg) in _winThreadMonitors)
{
if (reg.DisplayIdx > displayIdx)
reg.DisplayIdx -= 1;
}
SendEvent(new EventMonitorDestroy(monitorId));
}
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
{
_monitors.Remove(ev.Id);
_clyde._monitorHandles.Remove(ev.Id);
}
private sealed class Sdl2MonitorReg : MonitorReg
{
public int Id;
}
private sealed class WinThreadMonitorReg
{
public int Id;
public int DisplayIdx;
}
}
}

View File

@@ -1,316 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Robust.Shared.Maths;
using TerraFX.Interop.Windows;
using static SDL2.SDL;
using static SDL2.SDL.SDL_EventType;
using static SDL2.SDL.SDL_SYSWM_TYPE;
using static SDL2.SDL.SDL_WindowEventID;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
private static unsafe int EventWatch(void* userdata, SDL_Event* sdlevent)
{
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
ref readonly var ev = ref *sdlevent;
obj.ProcessSdl2Event(in ev);
return 0;
}
private void ProcessSdl2Event(in SDL_Event ev)
{
switch (ev.type)
{
case SDL_WINDOWEVENT:
ProcessSdl2EventWindow(in ev.window);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
ProcessSdl2KeyEvent(in ev.key);
break;
case SDL_TEXTINPUT:
ProcessSdl2EventTextInput(in ev.text);
break;
case SDL_TEXTEDITING:
ProcessSdl2EventTextEditing(in ev.edit);
break;
case SDL_KEYMAPCHANGED:
ProcessSdl2EventKeyMapChanged();
break;
case SDL_TEXTEDITING_EXT:
ProcessSdl2EventTextEditingExt(in ev.editExt);
break;
case SDL_MOUSEMOTION:
ProcessSdl2EventMouseMotion(in ev.motion);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
ProcessSdl2EventMouseButton(in ev.button);
break;
case SDL_MOUSEWHEEL:
ProcessSdl2EventMouseWheel(in ev.wheel);
break;
case SDL_DISPLAYEVENT:
ProcessSdl2EventDisplay(in ev.display);
break;
case SDL_SYSWMEVENT:
ProcessSdl2EventSysWM(in ev.syswm);
break;
case SDL_QUIT:
ProcessSdl2EventQuit();
break;
}
}
private void ProcessSdl2EventQuit()
{
SendEvent(new EventQuit());
}
private void ProcessSdl2EventDisplay(in SDL_DisplayEvent evDisplay)
{
switch (evDisplay.displayEvent)
{
case SDL_DisplayEventID.SDL_DISPLAYEVENT_CONNECTED:
WinThreadSetupMonitor((int) evDisplay.display);
break;
case SDL_DisplayEventID.SDL_DISPLAYEVENT_DISCONNECTED:
WinThreadDestroyMonitor((int) evDisplay.display);
break;
}
}
private void ProcessSdl2EventMouseWheel(in SDL_MouseWheelEvent ev)
{
SendEvent(new EventWheel(ev.windowID, ev.preciseX, ev.preciseY));
}
private void ProcessSdl2EventMouseButton(in SDL_MouseButtonEvent ev)
{
SendEvent(new EventMouseButton(ev.windowID, ev.type, ev.button));
}
private void ProcessSdl2EventMouseMotion(in SDL_MouseMotionEvent ev)
{
// _sawmill.Info($"{evMotion.x}, {evMotion.y}, {evMotion.xrel}, {evMotion.yrel}");
SendEvent(new EventMouseMotion(ev.windowID, ev.x, ev.y, ev.xrel, ev.yrel));
}
private unsafe void ProcessSdl2EventTextInput(in SDL_TextInputEvent ev)
{
fixed (byte* text = ev.text)
{
var str = Marshal.PtrToStringUTF8((IntPtr)text) ?? "";
// _logManager.GetSawmill("ime").Debug($"Input: {str}");
SendEvent(new EventText(ev.windowID, str));
}
}
private unsafe void ProcessSdl2EventTextEditing(in SDL_TextEditingEvent ev)
{
fixed (byte* text = ev.text)
{
SendTextEditing(ev.windowID, text, ev.start, ev.length);
}
}
private unsafe void ProcessSdl2EventTextEditingExt(in SDL_TextEditingExtEvent ev)
{
SendTextEditing(ev.windowID, (byte*) ev.text, ev.start, ev.length);
SDL_free(ev.text);
}
private unsafe void SendTextEditing(uint window, byte* text, int start, int length)
{
var str = Marshal.PtrToStringUTF8((nint) text) ?? "";
// _logManager.GetSawmill("ime").Debug($"Editing: '{str}', start: {start}, len: {length}");
SendEvent(new EventTextEditing(window, str, start, length));
}
private void ProcessSdl2EventKeyMapChanged()
{
ReloadKeyMap();
SendEvent(new EventKeyMapChanged());
}
private void ProcessSdl2KeyEvent(in SDL_KeyboardEvent ev)
{
SendEvent(new EventKey(
ev.windowID,
ev.keysym.scancode,
ev.type,
ev.repeat != 0,
ev.keysym.mod));
}
private void ProcessSdl2EventWindow(in SDL_WindowEvent ev)
{
var window = SDL_GetWindowFromID(ev.windowID);
switch (ev.windowEvent)
{
case SDL_WINDOWEVENT_SIZE_CHANGED:
var width = ev.data1;
var height = ev.data2;
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
var (xScale, yScale) = GetWindowScale(window);
_sawmill.Debug($"{width}x{height}, {fbW}x{fbH}, {xScale}x{yScale}");
SendEvent(new EventWindowSize(ev.windowID, width, height, fbW, fbH, xScale, yScale));
break;
default:
SendEvent(new EventWindow(ev.windowID, ev.windowEvent));
break;
}
}
// ReSharper disable once InconsistentNaming
private unsafe void ProcessSdl2EventSysWM(in SDL_SysWMEvent ev)
{
ref readonly var sysWmMessage = ref *(SDL_SysWMmsg*)ev.msg;
if (sysWmMessage.subsystem != SDL_SYSWM_WINDOWS)
return;
ref readonly var winMessage = ref *(SDL_SysWMmsgWin32*)ev.msg;
if (winMessage.msg is WM.WM_KEYDOWN or WM.WM_KEYUP)
{
TryWin32VirtualVKey(in winMessage);
}
}
private void TryWin32VirtualVKey(in SDL_SysWMmsgWin32 msg)
{
// Workaround for https://github.com/ocornut/imgui/issues/2977
// This is gonna bite me in the ass if SDL2 ever fixes this upstream, isn't it...
// (I spent disproportionate amounts of effort on this).
// Code for V key.
if ((int)msg.wParam is not (0x56 or VK.VK_CONTROL))
return;
var scanCode = (msg.lParam >> 16) & 0xFF;
if (scanCode != 0)
return;
SendEvent(new EventWindowsFakeV(msg.hwnd, msg.msg, msg.wParam));
}
private abstract record EventBase;
private record EventWindowCreate(
Sdl2WindowCreateResult Result,
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
) : EventBase;
private record EventKey(
uint WindowId,
SDL_Scancode Scancode,
SDL_EventType Type,
bool Repeat,
SDL_Keymod Mods
) : EventBase;
private record EventMouseMotion(
uint WindowId,
int X, int Y,
int XRel, int YRel
) : EventBase;
private record EventMouseButton(
uint WindowId,
SDL_EventType Type,
byte Button
) : EventBase;
private record EventText(
uint WindowId,
string Text
) : EventBase;
private record EventTextEditing(
uint WindowId,
string Text,
int Start,
int Length
) : EventBase;
private record EventWindowSize(
uint WindowId,
int Width,
int Height,
int FramebufferWidth,
int FramebufferHeight,
float XScale,
float YScale
) : EventBase;
private record EventWheel(
uint WindowId,
float XOffset,
float YOffset
) : EventBase;
// SDL_WindowEvents that don't have special handling like size.
private record EventWindow(
uint WindowId,
SDL_WindowEventID EventId
) : EventBase;
private record EventMonitorSetup
(
int Id,
string Name,
VideoMode CurrentMode,
VideoMode[] AllModes
) : EventBase;
private record EventMonitorDestroy
(
int Id
) : EventBase;
private record EventWindowsFakeV(HWND Window,
uint Message, WPARAM WParam) : EventBase;
private record EventKeyMapChanged : EventBase;
private record EventQuit : EventBase;
[StructLayout(LayoutKind.Sequential)]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
private struct SDL_SysWMmsg
{
public SDL_version version;
public SDL_SYSWM_TYPE subsystem;
}
[StructLayout(LayoutKind.Sequential)]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
private struct SDL_SysWMmsgWin32
{
public SDL_version version;
public SDL_SYSWM_TYPE subsystem;
public HWND hwnd;
public uint msg;
public WPARAM wParam;
public LPARAM lParam;
}
}
}

View File

@@ -1,586 +0,0 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static SDL2.SDL;
using static SDL2.SDL.SDL_bool;
using static SDL2.SDL.SDL_FlashOperation;
using static SDL2.SDL.SDL_GLattr;
using static SDL2.SDL.SDL_GLcontext;
using static SDL2.SDL.SDL_GLprofile;
using static SDL2.SDL.SDL_SYSWM_TYPE;
using static SDL2.SDL.SDL_WindowFlags;
using BOOL = TerraFX.Interop.Windows.BOOL;
using HWND = TerraFX.Interop.Windows.HWND;
using GWLP = TerraFX.Interop.Windows.GWLP;
using Windows = TerraFX.Interop.Windows.Windows;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
private int _nextWindowId = 1;
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner)
{
nint shareWindow = 0;
nint shareContext = 0;
if (share is Sdl2WindowReg shareReg)
{
shareWindow = shareReg.Sdl2Window;
shareContext = shareReg.GlContext;
}
nint ownerPtr = 0;
if (owner is Sdl2WindowReg ownerReg)
ownerPtr = ownerReg.Sdl2Window;
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
#pragma warning disable RA0004
// Block above ensured task is done, this is safe.
var (reg, error) = task.Result;
#pragma warning restore RA0004
if (reg != null)
{
reg.Owner = reg.Handle;
}
return (reg, error);
}
private void WaitWindowCreate(Task<Sdl2WindowCreateResult> windowTask)
{
while (!windowTask.IsCompleted)
{
// Keep processing events until the window task gives either an error or success.
WaitEvents();
ProcessEvents(single: true);
}
}
private Task<Sdl2WindowCreateResult> SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint owner)
{
//
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
// I originally wanted this to be async so we could avoid blocking the main thread
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
// This doesn't *work* because
// we have to release the GL context while the shared context is being created.
// (at least on WGL, I didn't test other platforms and I don't care to.)
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
// because rendering would be locked up *anyways*.
//
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
// and I should get on either Veldrid or Vulkan some time.
// Probably Veldrid tbh.
//
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<Sdl2WindowCreateResult>();
SendCmd(new CmdWinCreate(glSpec, parameters, shareWindow, shareContext, owner, tcs));
return tcs.Task;
}
private static void FinishWindowCreate(EventWindowCreate ev)
{
var (res, tcs) = ev;
tcs.TrySetResult(res);
}
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (glSpec, parameters, shareWindow, shareContext, owner, tcs) = cmd;
var (window, context) = CreateSdl2WindowForRenderer(glSpec, parameters, shareWindow, shareContext, owner);
if (window == 0)
{
var err = SDL_GetError();
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(null, err), tcs));
return;
}
// We can't invoke the TCS directly from the windowing thread because:
// * it'd hit the synchronization context,
// which would make (blocking) main window init more annoying.
// * it'd not be synchronized to other incoming window events correctly which might be icky.
// So we send the TCS back to the game thread
// which processes events in the correct order and has better control of stuff during init.
var reg = WinThreadSetupWindow(window, context);
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(reg, null), tcs));
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
{
if (OperatingSystem.IsWindows() && cmd.HadOwner)
{
// On Windows, closing the child window causes the owner to be minimized, apparently.
// Clear owner on close to avoid this.
SDL_SysWMinfo wmInfo = default;
SDL_VERSION(out wmInfo.version);
if (SDL_GetWindowWMInfo(cmd.Window, ref wmInfo) == SDL_TRUE && wmInfo.subsystem == SDL_SYSWM_WINDOWS)
{
var hWnd = (HWND)wmInfo.info.win.window;
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWLP.GWLP_HWNDPARENT,
0);
}
}
SDL_DestroyWindow(cmd.Window);
}
private (nint window, nint context) CreateSdl2WindowForRenderer(
GLContextSpec? spec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint ownerWindow)
{
var windowFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
if (spec is { } s)
{
windowFlags |= SDL_WINDOW_OPENGL;
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, s.Profile == GLContextProfile.Es ? 0 : 1);
SDL_GLcontext ctxFlags = 0;
#if DEBUG
ctxFlags |= SDL_GL_CONTEXT_DEBUG_FLAG;
#endif
if (s.Profile == GLContextProfile.Core)
ctxFlags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
if (shareContext != 0)
{
SDL_GL_MakeCurrent(shareWindow, shareContext);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
}
else
{
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
}
var profile = s.Profile switch
{
GLContextProfile.Compatibility => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
GLContextProfile.Core => SDL_GL_CONTEXT_PROFILE_CORE,
GLContextProfile.Es => SDL_GL_CONTEXT_PROFILE_ES,
_ => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
};
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile);
SDL_SetHint("SDL_OPENGL_ES_DRIVER", s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
if (s.CreationApi == GLContextCreationApi.Egl)
WsiShared.EnsureEglAvailable();
}
if (OperatingSystem.IsMacOS())
{
windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
}
if (parameters.Fullscreen)
{
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
nint window = SDL_CreateWindow(
"",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
parameters.Width, parameters.Height,
windowFlags);
if (window == 0)
return default;
nint glContext = SDL_GL_CreateContext(window);
if (glContext == 0)
{
SDL_DestroyWindow(window);
return default;
}
// TODO: Monitors, window maximize.
// TODO: a bunch of win32 calls for funny window properties I still haven't ported to other platforms.
// Make sure window thread doesn't keep hold of the GL context.
SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (OperatingSystem.IsWindows())
{
SDL_SysWMinfo info = default;
SDL_VERSION(out info.version);
if (SDL_GetWindowWMInfo(window, ref info) == SDL_TRUE && info.subsystem == SDL_SYSWM_WINDOWS)
WsiShared.WindowsSharedWindowCreate((HWND) info.info.win.window, _cfg);
}
if (parameters.Visible)
SDL_ShowWindow(window);
return (window, glContext);
}
private unsafe Sdl2WindowReg WinThreadSetupWindow(nint window, nint context)
{
var reg = new Sdl2WindowReg
{
Sdl2Window = window,
GlContext = context,
WindowId = SDL_GetWindowID(window),
Id = new WindowId(_nextWindowId++)
};
var handle = new WindowHandle(_clyde, reg);
reg.Handle = handle;
SDL_VERSION(out reg.SysWMinfo.version);
var res = SDL_GetWindowWMInfo(window, ref reg.SysWMinfo);
if (res == SDL_FALSE)
_sawmill.Error("Failed to get window WM info: {error}", SDL_GetError());
// LoadWindowIcon(window);
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
reg.FramebufferSize = (fbW, fbH);
reg.WindowScale = GetWindowScale(window);
SDL_GetWindowSize(window, out var w, out var h);
reg.PrevWindowSize = reg.WindowSize = (w, h);
SDL_GetWindowPosition(window, out var x, out var y);
reg.PrevWindowPos = (x, y);
reg.PixelRatio = reg.FramebufferSize / (Vector2) reg.WindowSize;
return reg;
}
public void WindowDestroy(WindowReg window)
{
var reg = (Sdl2WindowReg) window;
SendCmd(new CmdWinDestroy(reg.Sdl2Window, window.Owner != null));
}
public void UpdateMainWindowMode()
{
if (_clyde._mainWindow == null)
return;
var win = (Sdl2WindowReg) _clyde._mainWindow;
SendCmd(new CmdWinWinSetMode(win.Sdl2Window, _clyde._windowMode));
}
private static void WinThreadWinSetMode(CmdWinWinSetMode cmd)
{
var flags = cmd.Mode switch
{
WindowMode.Fullscreen => (uint) SDL_WINDOW_FULLSCREEN_DESKTOP,
_ => 0u
};
SDL_SetWindowFullscreen(cmd.Window, flags);
}
public void WindowSetTitle(WindowReg window, string title)
{
SendCmd(new CmdWinSetTitle(WinPtr(window), title));
}
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
{
SDL_SetWindowTitle(cmd.Window, cmd.Title);
}
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
{
// API isn't really used and kinda wack, don't feel like figuring it out for SDL2 yet.
_sawmill.Warning("WindowSetMonitor not implemented on SDL2");
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
SendCmd(new CmdWinSetSize(WinPtr(window), size.X, size.Y));
}
public void WindowSetVisible(WindowReg window, bool visible)
{
window.IsVisible = visible;
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
}
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
if (cmd.Visible)
SDL_ShowWindow(cmd.Window);
else
SDL_HideWindow(cmd.Window);
}
public void WindowRequestAttention(WindowReg window)
{
SendCmd(new CmdWinRequestAttention(WinPtr(window)));
}
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
{
var res = SDL_FlashWindow(cmd.Window, SDL_FLASH_UNTIL_FOCUSED);
if (res < 0)
_sawmill.Error("Failed to flash window: {error}", SDL_GetError());
}
public unsafe void WindowSwapBuffers(WindowReg window)
{
var reg = (Sdl2WindowReg)window;
var windowPtr = WinPtr(reg);
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL2 does not.
//
// Windows DwmFlush logic partly taken from:
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
// See also: https://github.com/libsdl-org/SDL/issues/5797
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
{
var curCtx = SDL_GL_GetCurrentContext();
var curWin = SDL_GL_GetCurrentWindow();
if (curCtx != reg.GlContext || curWin != reg.Sdl2Window)
throw new InvalidOperationException("Window context must be current!");
SDL_GL_SetSwapInterval(0);
dwmFlush = true;
swapInterval = reg.SwapInterval;
}
}
SDL_GL_SwapWindow(windowPtr);
if (dwmFlush)
{
var i = swapInterval;
while (i-- > 0)
{
Windows.DwmFlush();
}
SDL_GL_SetSwapInterval(swapInterval);
}
}
public uint? WindowGetX11Id(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
return null;
return (uint?) reg.SysWMinfo.info.x11.window;
}
public nint? WindowGetX11Display(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
return null;
return reg.SysWMinfo.info.x11.display;
}
public nint? WindowGetWin32Window(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_WINDOWS)
return null;
return reg.SysWMinfo.info.win.window;
}
public void RunOnWindowThread(Action a)
{
SendCmd(new CmdRunAction(a));
}
public void TextInputSetRect(UIBox2i rect)
{
SendCmd(new CmdTextInputSetRect(new SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
}));
}
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
{
var rect = cmdTextInput.Rect;
SDL_SetTextInputRect(ref rect);
}
public void TextInputStart()
{
SendCmd(CmdTextInputStart.Instance);
}
private static void WinThreadStartTextInput()
{
SDL_StartTextInput();
}
public void TextInputStop()
{
SendCmd(CmdTextInputStop.Instance);
}
private static void WinThreadStopTextInput()
{
SDL_StopTextInput();
}
public void ClipboardSetText(WindowReg mainWindow, string text)
{
SendCmd(new CmdSetClipboard(text));
}
private void WinThreadSetClipboard(CmdSetClipboard cmd)
{
var res = SDL_SetClipboardText(cmd.Text);
if (res < 0)
_sawmill.Error("Failed to set clipboard text: {error}", SDL_GetError());
}
public Task<string> ClipboardGetText(WindowReg mainWindow)
{
var tcs = new TaskCompletionSource<string>();
SendCmd(new CmdGetClipboard(tcs));
return tcs.Task;
}
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
{
cmd.Tcs.TrySetResult(SDL_GetClipboardText());
}
private static Vector2 GetWindowScale(nint window)
{
// Get scale by diving size in pixels with size in points.
SDL_GetWindowSizeInPixels(window, out var pixW, out var pixH);
SDL_GetWindowSize(window, out var pointW, out var pointH);
// Avoiding degenerate cases, not sure if these can actually happen.
if (pixW == 0 || pixH == 0 || pointW == 0 || pointH == 0)
return new Vector2(1, 1);
var scaleH = pixW / (float) pointW;
var scaleV = pixH / (float) pointH;
// Round to 5% increments to avoid rounding errors causing constantly different scales.
scaleH = MathF.Round(scaleH * 20) / 20;
scaleV = MathF.Round(scaleV * 20) / 20;
return new Vector2(scaleH, scaleV);
}
private static void CheckWindowDisposed(WindowReg reg)
{
if (reg.IsDisposed)
throw new ObjectDisposedException("Window disposed");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nint WinPtr(WindowReg reg) => ((Sdl2WindowReg)reg).Sdl2Window;
private WindowReg? FindWindow(uint windowId)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (Sdl2WindowReg) windowReg;
if (glfwReg.WindowId == windowId)
return windowReg;
}
return null;
}
private sealed class Sdl2WindowReg : WindowReg
{
public nint Sdl2Window;
public uint WindowId;
public nint GlContext;
public SDL_SysWMinfo SysWMinfo;
#pragma warning disable CS0649
public bool Fullscreen;
#pragma warning restore CS0649
public int SwapInterval;
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
}
}
}

View File

@@ -1,190 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using static SDL2.SDL;
using static SDL2.SDL.SDL_LogCategory;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly Clyde _clyde;
private GCHandle _selfGCHandle;
private readonly ISawmill _sawmill;
private readonly ISawmill _sawmillSdl2;
public Sdl2WindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;
deps.InjectDependencies(this, true);
_sawmill = _logManager.GetSawmill("clyde.win");
_sawmillSdl2 = _logManager.GetSawmill("clyde.win.sdl2");
}
public bool Init()
{
InitChannels();
if (!InitSdl2())
return false;
return true;
}
private unsafe bool InitSdl2()
{
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
SDL_LogSetAllPriority(SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
SDL_LogSetOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
SDL_SetHint("SDL_WINDOWS_DPI_SCALING", "1");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1");
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
var res = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
if (res < 0)
{
_sawmill.Fatal("Failed to initialize SDL2: {error}", SDL_GetError());
return false;
}
SDL_GetVersion(out var version);
var videoDriver = SDL_GetCurrentVideoDriver();
_sawmill.Debug(
"SDL2 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}", version.major, version.minor, version.patch, videoDriver);
_sdlEventWakeup = SDL_RegisterEvents(1);
SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE);
InitCursors();
InitMonitors();
ReloadKeyMap();
SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
// SDL defaults to having text input enabled, so we have to manually turn it off in init for consistency.
// If we don't, text input will remain enabled *until* the user first leaves a LineEdit/TextEdit.
SDL_StopTextInput();
return true;
}
public unsafe void Shutdown()
{
if (_selfGCHandle != default)
{
SDL_DelEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
_selfGCHandle.Free();
}
SDL_LogSetOutputFunction(null, null);
if (SDL_WasInit(0) != 0)
{
_sawmill.Debug("Terminating SDL2");
SDL_Quit();
}
}
public void FlushDispose()
{
// Not currently used
}
public void GLMakeContextCurrent(WindowReg? reg)
{
int res;
if (reg is Sdl2WindowReg sdlReg)
res = SDL_GL_MakeCurrent(sdlReg.Sdl2Window, sdlReg.GlContext);
else
res = SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (res < 0)
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL_GetError());
}
public void GLSwapInterval(WindowReg reg, int interval)
{
((Sdl2WindowReg)reg).SwapInterval = interval;
SDL_GL_SetSwapInterval(interval);
}
public unsafe void* GLGetProcAddress(string procName)
{
return (void*) SDL_GL_GetProcAddress(procName);
}
public string GetDescription()
{
SDL_GetVersion(out var version);
_sawmill.Debug(
"SDL2 initialized, version: {major}.{minor}.{patch}", version.major, version.minor, version.patch);
var videoDriver = SDL_GetCurrentVideoDriver();
return $"SDL2 {version.major}.{version.minor}.{version.patch} ({videoDriver})";
}
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
private static unsafe void LogOutputFunction(
void* userdata,
int category,
SDL_LogPriority priority,
byte* message)
{
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
var level = priority switch
{
SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
_ => LogLevel.Error
};
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
if (msg == "That operation is not supported")
{
obj._sawmillSdl2.Info(Environment.StackTrace);
}
var categoryName = SdlLogCategoryName(category);
obj._sawmillSdl2.Log(level, $"[{categoryName}] {msg}");
}
private static string SdlLogCategoryName(int category)
{
return (SDL_LogCategory) category switch {
// @formatter:off
SDL_LOG_CATEGORY_APPLICATION => "application",
SDL_LOG_CATEGORY_ERROR => "error",
SDL_LOG_CATEGORY_ASSERT => "assert",
SDL_LOG_CATEGORY_SYSTEM => "system",
SDL_LOG_CATEGORY_AUDIO => "audio",
SDL_LOG_CATEGORY_VIDEO => "video",
SDL_LOG_CATEGORY_RENDER => "render",
SDL_LOG_CATEGORY_INPUT => "input",
SDL_LOG_CATEGORY_TEST => "test",
_ => "unknown"
// @formatter:on
};
}
}
}

View File

@@ -3,16 +3,15 @@ using System.Collections.Generic;
using Robust.Client.Utility;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static SDL2.SDL;
using static SDL2.SDL.SDL_SystemCursor;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
private sealed partial class Sdl3WindowingImpl
{
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
private readonly CursorImpl[] _standardCursors = new CursorImpl[(int)StandardCursorShape.CountCursors];
@@ -28,31 +27,32 @@ internal partial class Clyde
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
var id = _clyde.AllocRid();
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
SendCmd(new CmdCursorCreate { Bytes = cloneImg, Hotspot = hotSpot, Cursor = id });
return new CursorImpl(this, id, false);
}
private unsafe void WinThreadCursorCreate(CmdCursorCreate cmd)
{
var (img, (hotX, hotY), id) = cmd;
using var img = cmd.Bytes;
fixed (Rgba32* pixPtr = img.GetPixelSpan())
{
var surface = SDL_CreateRGBSurfaceWithFormatFrom(
var surface = SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
(IntPtr)pixPtr,
img.Width, img.Height, 0,
sizeof(Rgba32) * img.Width,
SDL_PIXELFORMAT_RGBA8888);
sizeof(Rgba32) * img.Width);
var cursor = SDL_CreateColorCursor(surface, hotX, hotY);
var cursor = SDL.SDL_CreateColorCursor(surface, cmd.Hotspot.X, cmd.Hotspot.Y);
if (cursor == 0)
throw new InvalidOperationException("SDL_CreateColorCursor failed");
_winThreadCursors.Add(id, new WinThreadCursorReg { Ptr = cursor });
_winThreadCursors.Add(cmd.Cursor, new WinThreadCursorReg { Ptr = cursor });
SDL_FreeSurface(surface);
SDL.SDL_DestroySurface(surface);
}
img.Dispose();
}
public void CursorSet(WindowReg window, ICursor? cursor)
@@ -62,7 +62,7 @@ internal partial class Clyde
// SDL_SetCursor(NULL) does redraw, not reset.
cursor ??= CursorGetStandard(StandardCursorShape.Arrow);
var reg = (Sdl2WindowReg)window;
var reg = (Sdl3WindowReg)window;
if (reg.Cursor == cursor)
return;
@@ -74,7 +74,7 @@ internal partial class Clyde
throw new ObjectDisposedException(nameof(cursor));
reg.Cursor = impl;
SendCmd(new CmdWinCursorSet(reg.Sdl2Window, impl.Id));
SendCmd(new CmdWinCursorSet { Window = reg.Sdl3Window, Cursor = impl.Id });
}
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
@@ -83,22 +83,22 @@ internal partial class Clyde
var ptr = _winThreadCursors[cmd.Cursor].Ptr;
// TODO: multi-window??
SDL_SetCursor(ptr);
SDL.SDL_SetCursor(ptr);
}
private void InitCursors()
{
Add(StandardCursorShape.Arrow, SDL_SYSTEM_CURSOR_ARROW);
Add(StandardCursorShape.IBeam, SDL_SYSTEM_CURSOR_IBEAM);
Add(StandardCursorShape.Crosshair, SDL_SYSTEM_CURSOR_CROSSHAIR);
Add(StandardCursorShape.Hand, SDL_SYSTEM_CURSOR_HAND);
Add(StandardCursorShape.HResize, SDL_SYSTEM_CURSOR_SIZEWE);
Add(StandardCursorShape.VResize, SDL_SYSTEM_CURSOR_SIZENS);
Add(StandardCursorShape.Arrow, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_DEFAULT);
Add(StandardCursorShape.IBeam, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_TEXT);
Add(StandardCursorShape.Crosshair, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_CROSSHAIR);
Add(StandardCursorShape.Hand, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_POINTER);
Add(StandardCursorShape.HResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_EW_RESIZE);
Add(StandardCursorShape.VResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NS_RESIZE);
void Add(StandardCursorShape shape, SDL_SystemCursor sysCursor)
void Add(StandardCursorShape shape, SDL.SDL_SystemCursor sysCursor)
{
var id = _clyde.AllocRid();
var cursor = SDL_CreateSystemCursor(sysCursor);
var cursor = SDL.SDL_CreateSystemCursor(sysCursor);
var impl = new CursorImpl(this, id, true);
@@ -107,13 +107,21 @@ internal partial class Clyde
}
}
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
{
if (!_winThreadCursors.TryGetValue(cmd.Cursor, out var cursor))
return;
SDL.SDL_DestroyCursor(cursor.Ptr);
}
private sealed class CursorImpl : ICursor
{
private readonly bool _standard;
public Sdl2WindowingImpl Owner { get; }
public Sdl3WindowingImpl Owner { get; }
public ClydeHandle Id { get; private set; }
public CursorImpl(Sdl2WindowingImpl clyde, ClydeHandle id, bool standard)
public CursorImpl(Sdl3WindowingImpl clyde, ClydeHandle id, bool standard)
{
_standard = standard;
Owner = clyde;
@@ -127,7 +135,7 @@ internal partial class Clyde
private void DisposeImpl()
{
Owner.SendCmd(new CmdCursorDestroy(Id));
Owner.SendCmd(new CmdCursorDestroy { Cursor = Id });
Id = default;
}
@@ -147,9 +155,5 @@ internal partial class Clyde
{
public nint Ptr;
}
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
{
}
}
}

View File

@@ -2,18 +2,16 @@
using System.Numerics;
using Robust.Client.Input;
using Robust.Shared.Map;
using TerraFX.Interop.Windows;
using static SDL2.SDL;
using static SDL2.SDL.SDL_EventType;
using static SDL2.SDL.SDL_Keymod;
using static SDL2.SDL.SDL_WindowEventID;
using SDL3;
using Key = Robust.Client.Input.Keyboard.Key;
using ET = SDL3.SDL.SDL_EventType;
using SDL_Keymod = SDL3.SDL.SDL_Keymod;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
private sealed partial class Sdl3WindowingImpl
{
public void ProcessEvents(bool single = false)
{
@@ -47,15 +45,18 @@ internal partial class Clyde
case EventWindowCreate wCreate:
FinishWindowCreate(wCreate);
break;
case EventWindow ev:
ProcessEventWindow(ev);
case EventWindowMisc ev:
ProcessEventWindowMisc(ev);
break;
case EventKey ev:
ProcessEventKey(ev);
break;
case EventWindowSize ev:
case EventWindowPixelSize ev:
ProcessEventWindowSize(ev);
break;
case EventWindowContentScale ev:
ProcessEventWindowContentScale(ev);
break;
case EventText ev:
ProcessEventText(ev);
break;
@@ -74,9 +75,6 @@ internal partial class Clyde
case EventMonitorSetup ev:
ProcessSetupMonitor(ev);
break;
case EventWindowsFakeV ev:
ProcessWindowsFakeV(ev);
break;
case EventKeyMapChanged:
ProcessKeyMapChanged();
break;
@@ -84,7 +82,7 @@ internal partial class Clyde
ProcessEventQuit();
break;
default:
_sawmill.Error($"Unknown SDL2 event type: {evb.GetType().Name}");
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
break;
}
}
@@ -96,7 +94,7 @@ internal partial class Clyde
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
}
private void ProcessEventWindow(EventWindow ev)
private void ProcessEventWindowMisc(EventWindowMisc ev)
{
var window = FindWindow(ev.WindowId);
if (window == null)
@@ -104,33 +102,38 @@ internal partial class Clyde
switch (ev.EventId)
{
case SDL_WINDOWEVENT_CLOSE:
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
break;
case SDL_WINDOWEVENT_ENTER:
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
_clyde._currentHoveredWindow = window;
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, true));
break;
case SDL_WINDOWEVENT_LEAVE:
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
if (_clyde._currentHoveredWindow == window)
_clyde._currentHoveredWindow = null;
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, false));
break;
case SDL_WINDOWEVENT_MINIMIZED:
case ET.SDL_EVENT_WINDOW_MINIMIZED:
window.IsMinimized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
case ET.SDL_EVENT_WINDOW_RESTORED:
window.IsMinimized = false;
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
window.IsFocused = true;
_clyde.SendWindowFocus(new WindowFocusedEventArgs(true, window.Handle));
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
window.IsFocused = false;
_clyde.SendWindowFocus(new WindowFocusedEventArgs(false, window.Handle));
break;
case ET.SDL_EVENT_WINDOW_MOVED:
window.WindowPos = (ev.Data1, ev.Data2);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
@@ -153,10 +156,9 @@ internal partial class Clyde
if (windowReg == null)
return;
var mods = SDL_GetModState();
var button = ConvertSdl2Button(ev.Button);
var button = ConvertSdl3Button(ev.Button);
var key = Mouse.MouseButtonToKey(button);
EmitKeyEvent(key, ev.Type, false, mods, 0);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
}
private void ProcessEventMouseMotion(EventMouseMotion ev)
@@ -166,8 +168,7 @@ internal partial class Clyde
return;
var newPos = new Vector2(ev.X, ev.Y) * windowReg.PixelRatio;
// SDL2 does give us delta positions, but I'm worried about rounding errors thanks to DPI stuff.
var delta = newPos - windowReg.LastMousePos;
var delta = new Vector2(ev.XRel, ev.YRel);
windowReg.LastMousePos = newPos;
_clyde._currentHoveredWindow = windowReg;
@@ -185,7 +186,7 @@ internal partial class Clyde
_clyde.SendTextEditing(new TextEditingEventArgs(ev.Text, ev.Start, ev.Length));
}
private void ProcessEventWindowSize(EventWindowSize ev)
private void ProcessEventWindowSize(EventWindowPixelSize ev)
{
var window = ev.WindowId;
var width = ev.Width;
@@ -205,28 +206,31 @@ internal partial class Clyde
return;
windowReg.PixelRatio = windowReg.FramebufferSize / (Vector2)windowReg.WindowSize;
var newScale = new Vector2(ev.XScale, ev.YScale);
if (!windowReg.WindowScale.Equals(newScale))
{
windowReg.WindowScale = newScale;
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
}
_clyde.SendWindowResized(windowReg, oldSize);
}
private void ProcessEventKey(EventKey ev)
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
{
EmitKeyEvent(ConvertSdl2Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
var windowReg = FindWindow(ev.WindowId);
if (windowReg == null)
return;
windowReg.WindowScale = new Vector2(ev.Scale, ev.Scale);
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
}
private void EmitKeyEvent(Key key, SDL_EventType type, bool repeat, SDL_Keymod mods, SDL_Scancode scancode)
private void ProcessEventKey(EventKey ev)
{
var shift = (mods & KMOD_SHIFT) != 0;
var alt = (mods & KMOD_ALT) != 0;
var control = (mods & KMOD_CTRL) != 0;
var system = (mods & KMOD_GUI) != 0;
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
}
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
{
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
var control = (mods & SDL_Keymod.SDL_KMOD_CTRL) != 0;
var system = (mods & SDL_Keymod.SDL_KMOD_GUI) != 0;
var ev = new KeyEventArgs(
key,
@@ -236,36 +240,17 @@ internal partial class Clyde
switch (type)
{
case SDL_KEYUP:
case SDL_MOUSEBUTTONUP:
case ET.SDL_EVENT_KEY_UP:
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
_clyde.SendKeyUp(ev);
break;
case SDL_KEYDOWN:
case SDL_MOUSEBUTTONDOWN:
case ET.SDL_EVENT_KEY_DOWN:
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
_clyde.SendKeyDown(ev);
break;
}
}
private void ProcessWindowsFakeV(EventWindowsFakeV ev)
{
var type = (int)ev.Message switch
{
WM.WM_KEYUP => SDL_KEYUP,
WM.WM_KEYDOWN => SDL_KEYDOWN,
_ => throw new ArgumentOutOfRangeException()
};
var key = (int)ev.WParam switch
{
0x56 /* V */ => Key.V,
VK.VK_CONTROL => Key.Control,
_ => throw new ArgumentOutOfRangeException()
};
EmitKeyEvent(key, type, false, 0, 0);
}
private void ProcessKeyMapChanged()
{
_clyde.SendInputModeChanged();

View File

@@ -0,0 +1,145 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.UserInterface;
using SDL3;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
{
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
if (fileName == null)
return null;
return File.OpenRead(fileName);
}
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
if (fileName == null)
return null;
try
{
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(fileName, FileMode.Create), false);
}
}
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
{
var props = SDL.SDL_CreateProperties();
SDL.SDL_DialogFileFilter* filtersAlloc = null;
if (filters != null)
{
filtersAlloc = (SDL.SDL_DialogFileFilter*)NativeMemory.Alloc(
(UIntPtr)filters.Groups.Count,
(UIntPtr)sizeof(SDL.SDL_DialogFileFilter));
SDL.SDL_SetNumberProperty(props, SDL.SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, filters.Groups.Count);
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (nint)filtersAlloc);
// All these mallocs aren't gonna win any performance awards, but oh well.
for (var i = 0; i < filters.Groups.Count; i++)
{
var (name, pattern) = ConvertFilterGroup(filters.Groups[i]);
filtersAlloc[i].name = StringToNative(name);
filtersAlloc[i].pattern = StringToNative(pattern);
}
}
var task = ShowFileDialogWithProperties(type, props);
SDL.SDL_DestroyProperties(props);
if (filtersAlloc != null)
{
for (var i = 0; i < filters!.Groups.Count; i++)
{
var filter = filtersAlloc[i];
NativeMemory.Free(filter.name);
NativeMemory.Free(filter.pattern);
}
}
return task;
}
private static unsafe byte* StringToNative(string str)
{
var byteCount = Encoding.UTF8.GetByteCount(str);
var mem = (byte*) NativeMemory.Alloc((nuint)(byteCount + 1));
Encoding.UTF8.GetBytes(str, new Span<byte>(mem, byteCount));
mem[byteCount] = 0; // null-terminate
return mem;
}
private (string name, string pattern) ConvertFilterGroup(FileDialogFilters.Group group)
{
var name = string.Join(", ", group.Extensions.Select(e => $"*.{e}"));
var pattern = string.Join(";", group.Extensions);
return (name, pattern);
}
private unsafe Task<string?> ShowFileDialogWithProperties(int type, uint properties)
{
var tcs = new TaskCompletionSource<string?>();
var gcHandle = GCHandle.Alloc(new FileDialogState
{
Parent = this,
Tcs = tcs
});
SDL.SDL_ShowFileDialogWithProperties(
type,
&FileDialogCallback,
(void*)GCHandle.ToIntPtr(gcHandle),
properties);
return tcs.Task;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void FileDialogCallback(void* userdata, byte** filelist, int filter)
{
var stateHandle = GCHandle.FromIntPtr((IntPtr)userdata);
var state = (FileDialogState)stateHandle.Target!;
stateHandle.Free();
if (filelist == null)
{
// Error
state.Parent._sawmill.Error("File dialog failed: {error}", SDL.SDL_GetError());
state.Tcs.SetResult(null);
return;
}
// Handles null (cancelled/none selected) transparently.
var str = Marshal.PtrToStringUTF8((nint) filelist[0]);
state.Tcs.SetResult(str);
}
private sealed class FileDialogState
{
public required Sdl3WindowingImpl Parent;
public required TaskCompletionSource<string?> Tcs;
}
}
}

View File

@@ -0,0 +1,225 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL3;
using Key = Robust.Client.Input.Keyboard.Key;
using Button = Robust.Client.Input.Mouse.Button;
using SC = SDL3.SDL.SDL_Scancode;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
// Indices are values of SDL_Scancode
private static readonly Key[] KeyMap;
private static readonly FrozenDictionary<Key, SC> KeyMapReverse;
private static readonly Button[] MouseButtonMap;
// TODO: to avoid having to ask the windowing thread, key names are cached.
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
private void ReloadKeyMap()
{
// This may be ran concurrently from the windowing thread.
lock (_printableKeyNameMap)
{
_printableKeyNameMap.Clear();
// TODO: Validate this is correct in SDL3.
// List of mappable keys from SDL2's source appears to be:
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
// (the actual logic is more nuanced, but it appears to match the above)
// Comes out to these two ranges:
for (var k = SC.SDL_SCANCODE_A; k <= SC.SDL_SCANCODE_0; k++)
{
CacheKey(k);
}
for (var k = SC.SDL_SCANCODE_MINUS; k <= SC.SDL_SCANCODE_SLASH; k++)
{
CacheKey(k);
}
void CacheKey(SC scancode)
{
var rKey = ConvertSdl3Scancode(scancode);
if (rKey == Key.Unknown)
return;
// TODO: SDL_GetKeyFromScancode correct?
var name = SDL.SDL_GetKeyName(
SDL.SDL_GetKeyFromScancode(scancode, SDL.SDL_Keymod.SDL_KMOD_NONE, false));
if (!string.IsNullOrEmpty(name))
_printableKeyNameMap.Add(rKey, name);
}
}
}
public string? KeyGetName(Key key)
{
lock (_printableKeyNameMap)
{
if (_printableKeyNameMap.TryGetValue(key, out var name))
return name;
return null;
}
}
internal static Key ConvertSdl3Scancode(SC scancode)
{
return KeyMap[(int) scancode];
}
public static Button ConvertSdl3Button(int button)
{
return MouseButtonMap[button];
}
static Sdl3WindowingImpl()
{
MouseButtonMap = new Button[6];
MouseButtonMap[SDL.SDL_BUTTON_LEFT] = Button.Left;
MouseButtonMap[SDL.SDL_BUTTON_RIGHT] = Button.Right;
MouseButtonMap[SDL.SDL_BUTTON_MIDDLE] = Button.Middle;
MouseButtonMap[SDL.SDL_BUTTON_X1] = Button.Button4;
MouseButtonMap[SDL.SDL_BUTTON_X2] = Button.Button5;
KeyMap = new Key[(int) SC.SDL_SCANCODE_COUNT];
MapKey(SC.SDL_SCANCODE_A, Key.A);
MapKey(SC.SDL_SCANCODE_B, Key.B);
MapKey(SC.SDL_SCANCODE_C, Key.C);
MapKey(SC.SDL_SCANCODE_D, Key.D);
MapKey(SC.SDL_SCANCODE_E, Key.E);
MapKey(SC.SDL_SCANCODE_F, Key.F);
MapKey(SC.SDL_SCANCODE_G, Key.G);
MapKey(SC.SDL_SCANCODE_H, Key.H);
MapKey(SC.SDL_SCANCODE_I, Key.I);
MapKey(SC.SDL_SCANCODE_J, Key.J);
MapKey(SC.SDL_SCANCODE_K, Key.K);
MapKey(SC.SDL_SCANCODE_L, Key.L);
MapKey(SC.SDL_SCANCODE_M, Key.M);
MapKey(SC.SDL_SCANCODE_N, Key.N);
MapKey(SC.SDL_SCANCODE_O, Key.O);
MapKey(SC.SDL_SCANCODE_P, Key.P);
MapKey(SC.SDL_SCANCODE_Q, Key.Q);
MapKey(SC.SDL_SCANCODE_R, Key.R);
MapKey(SC.SDL_SCANCODE_S, Key.S);
MapKey(SC.SDL_SCANCODE_T, Key.T);
MapKey(SC.SDL_SCANCODE_U, Key.U);
MapKey(SC.SDL_SCANCODE_V, Key.V);
MapKey(SC.SDL_SCANCODE_W, Key.W);
MapKey(SC.SDL_SCANCODE_X, Key.X);
MapKey(SC.SDL_SCANCODE_Y, Key.Y);
MapKey(SC.SDL_SCANCODE_Z, Key.Z);
MapKey(SC.SDL_SCANCODE_0, Key.Num0);
MapKey(SC.SDL_SCANCODE_1, Key.Num1);
MapKey(SC.SDL_SCANCODE_2, Key.Num2);
MapKey(SC.SDL_SCANCODE_3, Key.Num3);
MapKey(SC.SDL_SCANCODE_4, Key.Num4);
MapKey(SC.SDL_SCANCODE_5, Key.Num5);
MapKey(SC.SDL_SCANCODE_6, Key.Num6);
MapKey(SC.SDL_SCANCODE_7, Key.Num7);
MapKey(SC.SDL_SCANCODE_8, Key.Num8);
MapKey(SC.SDL_SCANCODE_9, Key.Num9);
MapKey(SC.SDL_SCANCODE_KP_0, Key.NumpadNum0);
MapKey(SC.SDL_SCANCODE_KP_1, Key.NumpadNum1);
MapKey(SC.SDL_SCANCODE_KP_2, Key.NumpadNum2);
MapKey(SC.SDL_SCANCODE_KP_3, Key.NumpadNum3);
MapKey(SC.SDL_SCANCODE_KP_4, Key.NumpadNum4);
MapKey(SC.SDL_SCANCODE_KP_5, Key.NumpadNum5);
MapKey(SC.SDL_SCANCODE_KP_6, Key.NumpadNum6);
MapKey(SC.SDL_SCANCODE_KP_7, Key.NumpadNum7);
MapKey(SC.SDL_SCANCODE_KP_8, Key.NumpadNum8);
MapKey(SC.SDL_SCANCODE_KP_9, Key.NumpadNum9);
MapKey(SC.SDL_SCANCODE_ESCAPE, Key.Escape);
MapKey(SC.SDL_SCANCODE_LCTRL, Key.Control);
MapKey(SC.SDL_SCANCODE_RCTRL, Key.Control);
MapKey(SC.SDL_SCANCODE_RSHIFT, Key.Shift);
MapKey(SC.SDL_SCANCODE_LSHIFT, Key.Shift);
MapKey(SC.SDL_SCANCODE_LALT, Key.Alt);
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
MapKey(SC.SDL_SCANCODE_COMMA, Key.Comma);
MapKey(SC.SDL_SCANCODE_PERIOD, Key.Period);
MapKey(SC.SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
MapKey(SC.SDL_SCANCODE_SLASH, Key.Slash);
MapKey(SC.SDL_SCANCODE_BACKSLASH, Key.BackSlash);
MapKey(SC.SDL_SCANCODE_GRAVE, Key.Tilde);
MapKey(SC.SDL_SCANCODE_EQUALS, Key.Equal);
MapKey(SC.SDL_SCANCODE_SPACE, Key.Space);
MapKey(SC.SDL_SCANCODE_RETURN, Key.Return);
MapKey(SC.SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
MapKey(SC.SDL_SCANCODE_BACKSPACE, Key.BackSpace);
MapKey(SC.SDL_SCANCODE_TAB, Key.Tab);
MapKey(SC.SDL_SCANCODE_PAGEUP, Key.PageUp);
MapKey(SC.SDL_SCANCODE_PAGEDOWN, Key.PageDown);
MapKey(SC.SDL_SCANCODE_END, Key.End);
MapKey(SC.SDL_SCANCODE_HOME, Key.Home);
MapKey(SC.SDL_SCANCODE_INSERT, Key.Insert);
MapKey(SC.SDL_SCANCODE_DELETE, Key.Delete);
MapKey(SC.SDL_SCANCODE_MINUS, Key.Minus);
MapKey(SC.SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
MapKey(SC.SDL_SCANCODE_UP, Key.Up);
MapKey(SC.SDL_SCANCODE_DOWN, Key.Down);
MapKey(SC.SDL_SCANCODE_F1, Key.F1);
MapKey(SC.SDL_SCANCODE_F2, Key.F2);
MapKey(SC.SDL_SCANCODE_F3, Key.F3);
MapKey(SC.SDL_SCANCODE_F4, Key.F4);
MapKey(SC.SDL_SCANCODE_F5, Key.F5);
MapKey(SC.SDL_SCANCODE_F6, Key.F6);
MapKey(SC.SDL_SCANCODE_F7, Key.F7);
MapKey(SC.SDL_SCANCODE_F8, Key.F8);
MapKey(SC.SDL_SCANCODE_F9, Key.F9);
MapKey(SC.SDL_SCANCODE_F10, Key.F10);
MapKey(SC.SDL_SCANCODE_F11, Key.F11);
MapKey(SC.SDL_SCANCODE_F12, Key.F12);
MapKey(SC.SDL_SCANCODE_F13, Key.F13);
MapKey(SC.SDL_SCANCODE_F14, Key.F14);
MapKey(SC.SDL_SCANCODE_F15, Key.F15);
MapKey(SC.SDL_SCANCODE_F16, Key.F16);
MapKey(SC.SDL_SCANCODE_F17, Key.F17);
MapKey(SC.SDL_SCANCODE_F18, Key.F18);
MapKey(SC.SDL_SCANCODE_F19, Key.F19);
MapKey(SC.SDL_SCANCODE_F20, Key.F20);
MapKey(SC.SDL_SCANCODE_F21, Key.F21);
MapKey(SC.SDL_SCANCODE_F22, Key.F22);
MapKey(SC.SDL_SCANCODE_F23, Key.F23);
MapKey(SC.SDL_SCANCODE_F24, Key.F24);
MapKey(SC.SDL_SCANCODE_PAUSE, Key.Pause);
var keyMapReverse = new Dictionary<Key, SC>();
for (var code = 0; code < KeyMap.Length; code++)
{
var key = KeyMap[code];
if (key != Key.Unknown)
keyMapReverse[key] = (SC) code;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void MapKey(SC code, Key key)
{
KeyMap[(int)code] = key;
}
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL3;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
// NOTE: SDL3 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
private int _nextMonitorId = 1;
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
private readonly Dictionary<int, Sdl3MonitorReg> _monitors = new();
private unsafe void InitMonitors()
{
var displayList = (uint*)SDL.SDL_GetDisplays(out var count);
for (var i = 0; i < count; i++)
{
WinThreadSetupMonitor(displayList[i]);
}
SDL.SDL_free((nint)displayList);
// Needed so that monitor creation events get processed.
ProcessEvents();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe void WinThreadSetupMonitor(uint displayId)
{
var id = _nextMonitorId++;
var name = SDL.SDL_GetDisplayName(displayId);
var modePtr = (SDL.SDL_DisplayMode**)SDL.SDL_GetFullscreenDisplayModes(displayId, out var modeCount);
var curMode = (SDL.SDL_DisplayMode*)SDL.SDL_GetCurrentDisplayMode(displayId);
var modes = new VideoMode[modeCount];
for (var i = 0; i < modes.Length; i++)
{
modes[i] = ConvertVideoMode(in *modePtr[i]);
}
SDL.SDL_free((nint)modePtr);
_winThreadMonitors.Add(id, new WinThreadMonitorReg { DisplayId = displayId });
if (SDL.SDL_GetPrimaryDisplay() == displayId)
_clyde._primaryMonitorId = id;
SendEvent(new EventMonitorSetup
{
Id = id,
DisplayId = displayId,
Name = name,
AllModes = modes,
CurrentMode = ConvertVideoMode(in *curMode),
});
}
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
{
return new()
{
Width = (ushort)mode.w,
Height = (ushort)mode.h,
RefreshRate = (ushort)mode.refresh_rate,
// TODO: set bits count based on format (I'm lazy)
RedBits = 8,
GreenBits = 8,
BlueBits = 8,
};
}
private void ProcessSetupMonitor(EventMonitorSetup ev)
{
var impl = new MonitorHandle(
ev.Id,
ev.Name,
(ev.CurrentMode.Width, ev.CurrentMode.Height),
ev.CurrentMode.RefreshRate,
ev.AllModes);
_clyde._monitorHandles.Add(ev.Id, impl);
_monitors[ev.Id] = new Sdl3MonitorReg
{
DisplayId = ev.DisplayId,
Handle = impl
};
}
private void WinThreadDestroyMonitor(uint displayId)
{
var monitorId = GetMonitorIdFromDisplayId(displayId);
if (monitorId == 0)
return;
_winThreadMonitors.Remove(monitorId);
SendEvent(new EventMonitorDestroy { Id = monitorId });
}
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
{
_monitors.Remove(ev.Id);
_clyde._monitorHandles.Remove(ev.Id);
}
private int GetMonitorIdFromDisplayId(uint displayId)
{
foreach (var (id, monitorReg) in _winThreadMonitors)
{
if (monitorReg.DisplayId == displayId)
{
return id;
}
}
return 0;
}
private sealed class Sdl3MonitorReg : MonitorReg
{
public uint DisplayId;
}
private sealed class WinThreadMonitorReg
{
public uint DisplayId;
}
}
}

View File

@@ -0,0 +1,282 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SDL3;
using ET = SDL3.SDL.SDL_EventType;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe byte EventWatch(void* userdata, SDL.SDL_Event* sdlevent)
{
var obj = (Sdl3WindowingImpl)GCHandle.FromIntPtr((IntPtr)userdata).Target!;
obj.ProcessSdl3Event(in *sdlevent);
return 0;
}
private void ProcessSdl3Event(in SDL.SDL_Event ev)
{
switch ((ET)ev.type)
{
case ET.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
ProcessSdl3EventWindowPixelSizeChanged(in ev.window);
break;
case ET.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
ProcessSdl3EventWindowDisplayScaleChanged(in ev.window);
break;
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
case ET.SDL_EVENT_WINDOW_MINIMIZED:
case ET.SDL_EVENT_WINDOW_RESTORED:
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
case ET.SDL_EVENT_WINDOW_MOVED:
ProcessSdl3EventWindowMisc(in ev.window);
break;
case ET.SDL_EVENT_KEY_DOWN:
case ET.SDL_EVENT_KEY_UP:
ProcessSdl3KeyEvent(in ev.key);
break;
case ET.SDL_EVENT_TEXT_INPUT:
ProcessSdl3EventTextInput(in ev.text);
break;
case ET.SDL_EVENT_TEXT_EDITING:
ProcessSdl3EventTextEditing(in ev.edit);
break;
case ET.SDL_EVENT_KEYMAP_CHANGED:
ProcessSdl3EventKeyMapChanged();
break;
case ET.SDL_EVENT_MOUSE_MOTION:
ProcessSdl3EventMouseMotion(in ev.motion);
break;
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
ProcessSdl3EventMouseButton(in ev.button);
break;
case ET.SDL_EVENT_MOUSE_WHEEL:
ProcessSdl3EventMouseWheel(in ev.wheel);
break;
case ET.SDL_EVENT_DISPLAY_ADDED:
WinThreadSetupMonitor(ev.display.displayID);
break;
case ET.SDL_EVENT_DISPLAY_REMOVED:
WinThreadDestroyMonitor(ev.display.displayID);
break;
case ET.SDL_EVENT_QUIT:
ProcessSdl3EventQuit();
break;
}
}
private void ProcessSdl3EventQuit()
{
SendEvent(new EventQuit());
}
private void ProcessSdl3EventMouseWheel(in SDL.SDL_MouseWheelEvent ev)
{
SendEvent(new EventWheel { WindowId = ev.windowID, XOffset = ev.x, YOffset = ev.y });
}
private void ProcessSdl3EventMouseButton(in SDL.SDL_MouseButtonEvent ev)
{
var mods = SDL.SDL_GetModState();
SendEvent(new EventMouseButton
{
WindowId = ev.windowID,
Type = ev.type,
Button = ev.button,
Mods = mods
});
}
private void ProcessSdl3EventMouseMotion(in SDL.SDL_MouseMotionEvent ev)
{
SendEvent(new EventMouseMotion
{
WindowId = ev.windowID,
X = ev.x,
Y = ev.y,
XRel = ev.xrel,
YRel = ev.yrel
});
}
private unsafe void ProcessSdl3EventTextInput(in SDL.SDL_TextInputEvent ev)
{
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
SendEvent(new EventText { WindowId = ev.windowID, Text = str });
}
private unsafe void ProcessSdl3EventTextEditing(in SDL.SDL_TextEditingEvent ev)
{
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
SendEvent(new EventTextEditing
{
WindowId = ev.windowID,
Text = str,
Start = ev.start,
Length = ev.length
});
}
private void ProcessSdl3EventKeyMapChanged()
{
ReloadKeyMap();
SendEvent(new EventKeyMapChanged());
}
private void ProcessSdl3KeyEvent(in SDL.SDL_KeyboardEvent ev)
{
SendEvent(new EventKey
{
WindowId = ev.windowID,
Scancode = ev.scancode,
Type = ev.type,
Repeat = ev.repeat,
Mods = ev.mod,
});
}
private void ProcessSdl3EventWindowPixelSizeChanged(in SDL.SDL_WindowEvent ev)
{
var window = SDL.SDL_GetWindowFromID(ev.windowID);
SDL.SDL_GetWindowSize(window, out var width, out var height);
var fbW = ev.data1;
var fbH = ev.data2;
SendEvent(new EventWindowPixelSize
{
WindowId = ev.windowID,
Width = width,
Height = height,
FramebufferWidth = fbW,
FramebufferHeight = fbH,
});
}
private void ProcessSdl3EventWindowDisplayScaleChanged(in SDL.SDL_WindowEvent ev)
{
var window = SDL.SDL_GetWindowFromID(ev.windowID);
var scale = SDL.SDL_GetWindowDisplayScale(window);
SendEvent(new EventWindowContentScale { WindowId = ev.windowID, Scale = scale });
}
private void ProcessSdl3EventWindowMisc(in SDL.SDL_WindowEvent ev)
{
SendEvent(new EventWindowMisc
{
WindowId = ev.windowID,
EventId = ev.type,
Data1 = ev.data1,
Data2 = ev.data2
});
}
private abstract class EventBase;
private sealed class EventWindowCreate : EventBase
{
public required Sdl3WindowCreateResult Result;
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
}
private sealed class EventKey : EventBase
{
public uint WindowId;
public SDL.SDL_Scancode Scancode;
public ET Type;
public bool Repeat;
public SDL.SDL_Keymod Mods;
}
private sealed class EventMouseMotion : EventBase
{
public uint WindowId;
public float X;
public float Y;
public float XRel;
public float YRel;
}
private sealed class EventMouseButton : EventBase
{
public uint WindowId;
public ET Type;
public byte Button;
public SDL.SDL_Keymod Mods;
}
private sealed class EventText : EventBase
{
public uint WindowId;
public required string Text;
}
private sealed class EventTextEditing : EventBase
{
public uint WindowId;
public required string Text;
public int Start;
public int Length;
}
private sealed class EventWindowPixelSize : EventBase
{
public uint WindowId;
public int Width;
public int Height;
public int FramebufferWidth;
public int FramebufferHeight;
}
private sealed class EventWindowContentScale : EventBase
{
public uint WindowId;
public float Scale;
}
private sealed class EventWheel : EventBase
{
public uint WindowId;
public float XOffset;
public float YOffset;
}
// SDL_WindowEvents that don't need any handling on the window thread itself.
private sealed class EventWindowMisc : EventBase
{
public uint WindowId;
public ET EventId;
public int Data1;
public int Data2;
}
private sealed class EventMonitorSetup : EventBase
{
public int Id;
public uint DisplayId;
public required string Name;
public VideoMode CurrentMode;
public required VideoMode[] AllModes;
}
private sealed class EventMonitorDestroy : EventBase
{
public int Id;
}
private sealed class EventKeyMapChanged : EventBase;
private sealed class EventQuit : EventBase;
}
}

View File

@@ -0,0 +1,654 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using SDL3;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
using BOOL = TerraFX.Interop.Windows.BOOL;
using Windows = TerraFX.Interop.Windows.Windows;
using GLAttr = SDL3.SDL.SDL_GLAttr;
using X11Window = TerraFX.Interop.Xlib.Window;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
private int _nextWindowId = 1;
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner)
{
nint shareWindow = 0;
nint shareContext = 0;
if (share is Sdl3WindowReg shareReg)
{
shareWindow = shareReg.Sdl3Window;
shareContext = shareReg.GlContext;
}
nint ownerPtr = 0;
if (owner is Sdl3WindowReg ownerReg)
ownerPtr = ownerReg.Sdl3Window;
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
#pragma warning disable RA0004
// Block above ensured task is done, this is safe.
var result = task.Result;
#pragma warning restore RA0004
if (result.Reg != null)
{
result.Reg.Owner = result.Reg.Handle;
}
return (result.Reg, result.Error);
}
private void WaitWindowCreate(Task<Sdl3WindowCreateResult> windowTask)
{
while (!windowTask.IsCompleted)
{
// Keep processing events until the window task gives either an error or success.
WaitEvents();
ProcessEvents(single: true);
}
}
private Task<Sdl3WindowCreateResult> SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint owner)
{
//
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
// I originally wanted this to be async so we could avoid blocking the main thread
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
// This doesn't *work* because
// we have to release the GL context while the shared context is being created.
// (at least on WGL, I didn't test other platforms and I don't care to.)
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
// because rendering would be locked up *anyways*.
//
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
// and I should get on either Veldrid or Vulkan some time.
// Probably Veldrid tbh.
//
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<Sdl3WindowCreateResult>();
SendCmd(new CmdWinCreate
{
GLSpec = glSpec,
Parameters = parameters,
ShareWindow = shareWindow,
ShareContext = shareContext,
OwnerWindow = owner,
Tcs = tcs
});
return tcs.Task;
}
private static void FinishWindowCreate(EventWindowCreate ev)
{
ev.Tcs.TrySetResult(ev.Result);
}
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (window, context) = CreateSdl3WindowForRenderer(
cmd.GLSpec,
cmd.Parameters,
cmd.ShareWindow,
cmd.ShareContext,
cmd.OwnerWindow);
if (window == 0)
{
var err = SDL.SDL_GetError();
SendEvent(new EventWindowCreate
{
Result = new Sdl3WindowCreateResult { Error = err },
Tcs = cmd.Tcs
});
return;
}
// We can't invoke the TCS directly from the windowing thread because:
// * it'd hit the synchronization context,
// which would make (blocking) main window init more annoying.
// * it'd not be synchronized to other incoming window events correctly which might be icky.
// So we send the TCS back to the game thread
// which processes events in the correct order and has better control of stuff during init.
var reg = WinThreadSetupWindow(window, context);
SendEvent(new EventWindowCreate
{
Result = new Sdl3WindowCreateResult { Reg = reg },
Tcs = cmd.Tcs
});
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
{
SDL.SDL_DestroyWindow(cmd.Window);
}
private (nint window, nint context) CreateSdl3WindowForRenderer(
GLContextSpec? spec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint ownerWindow)
{
var createProps = SDL.SDL_CreateProperties();
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
if (spec is { } s)
{
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_RED_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_GREEN_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_BLUE_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_ALPHA_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_STENCIL_SIZE, 8);
SDL.SDL_GL_SetAttribute(
GLAttr.SDL_GL_FRAMEBUFFER_SRGB_CAPABLE,
s.Profile == GLContextProfile.Es ? 0 : 1);
int ctxFlags = 0;
#if DEBUG
ctxFlags |= SDL.SDL_GL_CONTEXT_DEBUG_FLAG;
#endif
if (s.Profile == GLContextProfile.Core)
ctxFlags |= SDL.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
if (shareContext != 0)
{
SDL.SDL_GL_MakeCurrent(shareWindow, shareContext);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
}
else
{
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
}
var profile = s.Profile switch
{
GLContextProfile.Compatibility => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
GLContextProfile.Core => SDL.SDL_GL_CONTEXT_PROFILE_CORE,
GLContextProfile.Es => SDL.SDL_GL_CONTEXT_PROFILE_ES,
_ => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
};
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, profile);
SDL.SDL_SetHint(SDL.SDL_HINT_OPENGL_ES_DRIVER, s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
if (s.CreationApi == GLContextCreationApi.Egl)
WsiShared.EnsureEglAvailable();
}
if (parameters.Fullscreen)
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
if (ownerWindow != 0)
{
SDL.SDL_SetPointerProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_PARENT_POINTER, ownerWindow);
if (parameters.StartupLocation == WindowStartupLocation.CenterOwner)
{
SDL.SDL_GetWindowSize(ownerWindow, out var parentW, out var parentH);
SDL.SDL_GetWindowPosition(ownerWindow, out var parentX, out var parentY);
SDL.SDL_SetNumberProperty(
createProps,
SDL.SDL_PROP_WINDOW_CREATE_X_NUMBER,
parentX + (parentW - parameters.Width) / 2);
SDL.SDL_SetNumberProperty(
createProps,
SDL.SDL_PROP_WINDOW_CREATE_Y_NUMBER,
parentY + (parentH - parameters.Height) / 2);
}
}
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, parameters.Width);
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, parameters.Height);
SDL.SDL_SetStringProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_TITLE_STRING, parameters.Title);
// ---> CREATE <---
var window = SDL.SDL_CreateWindowWithProperties(createProps);
SDL.SDL_DestroyProperties(createProps);
if (window == 0)
return default;
nint glContext = SDL.SDL_GL_CreateContext(window);
if (glContext == 0)
{
SDL.SDL_DestroyWindow(window);
return default;
}
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
{
var props = SDL.SDL_GetWindowProperties(window);
switch (_videoDriver)
{
case SdlVideoDriver.Windows:
{
var hWnd = SDL.SDL_GetPointerProperty(
props,
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
0);
WsiShared.SetWindowStyleNoTitleOptionsWindows((HWND)hWnd);
break;
}
case SdlVideoDriver.X11:
unsafe
{
var x11Display = (Display*)SDL.SDL_GetPointerProperty(
props,
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
0);
var x11Window = (X11Window)SDL.SDL_GetNumberProperty(
props,
SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
0);
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
break;
}
default:
_sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this video driver");
break;
}
}
// TODO: Monitors, window maximize.
// Make sure window thread doesn't keep hold of the GL context.
SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (parameters.Visible)
SDL.SDL_ShowWindow(window);
return (window, glContext);
}
private Sdl3WindowReg WinThreadSetupWindow(nint window, nint context)
{
var reg = new Sdl3WindowReg
{
Sdl3Window = window,
GlContext = context,
WindowId = SDL.SDL_GetWindowID(window),
Id = new WindowId(_nextWindowId++)
};
var handle = new WindowHandle(_clyde, reg);
reg.Handle = handle;
var windowProps = SDL.SDL_GetWindowProperties(window);
switch (_videoDriver)
{
case SdlVideoDriver.Windows:
reg.WindowsHwnd = SDL.SDL_GetPointerProperty(
windowProps,
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
0);
break;
case SdlVideoDriver.X11:
reg.X11Display = SDL.SDL_GetPointerProperty(
windowProps,
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
0);
reg.X11Id = (uint)SDL.SDL_GetNumberProperty(windowProps, SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
break;
}
AssignWindowIconToWindow(window);
SDL.SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
reg.FramebufferSize = (fbW, fbH);
var scale = SDL.SDL_GetWindowDisplayScale(window);
reg.WindowScale = new Vector2(scale, scale);
SDL.SDL_GetWindowSize(window, out var w, out var h);
reg.PrevWindowSize = reg.WindowSize = (w, h);
SDL.SDL_GetWindowPosition(window, out var x, out var y);
reg.PrevWindowPos = reg.WindowPos = (x, y);
reg.PixelRatio = reg.FramebufferSize / (Vector2)reg.WindowSize;
return reg;
}
public void WindowDestroy(WindowReg window)
{
var reg = (Sdl3WindowReg)window;
SendCmd(new CmdWinDestroy
{
Window = reg.Sdl3Window,
HadOwner = window.Owner != null
});
}
public void UpdateMainWindowMode()
{
if (_clyde._mainWindow == null)
return;
var win = (Sdl3WindowReg)_clyde._mainWindow;
if (_clyde._windowMode == WindowMode.Fullscreen)
{
win.PrevWindowSize = win.WindowSize;
win.PrevWindowPos = win.WindowPos;
SendCmd(new CmdWinWinSetFullscreen
{
Window = win.Sdl3Window,
});
}
else
{
SendCmd(new CmdWinSetWindowed
{
Window = win.Sdl3Window,
Width = win.PrevWindowSize.X,
Height = win.PrevWindowSize.Y,
PosX = win.PrevWindowPos.X,
PosY = win.PrevWindowPos.Y
});
}
}
private static void WinThreadWinSetFullscreen(CmdWinWinSetFullscreen cmd)
{
SDL.SDL_SetWindowFullscreen(cmd.Window, true);
}
private static void WinThreadWinSetWindowed(CmdWinSetWindowed cmd)
{
SDL.SDL_SetWindowFullscreen(cmd.Window, false);
SDL.SDL_SetWindowSize(cmd.Window, cmd.Width, cmd.Height);
SDL.SDL_SetWindowPosition(cmd.Window, cmd.PosX, cmd.PosY);
}
public void WindowSetTitle(WindowReg window, string title)
{
SendCmd(new CmdWinSetTitle
{
Window = WinPtr(window),
Title = title,
});
}
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
{
SDL.SDL_SetWindowTitle(cmd.Window, cmd.Title);
}
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
{
// API isn't really used and kinda wack, don't feel like figuring it out for SDL3 yet.
_sawmill.Warning("WindowSetMonitor not implemented on SDL3");
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
SendCmd(new CmdWinSetSize { Window = WinPtr(window), W = size.X, H = size.Y });
}
public void WindowSetVisible(WindowReg window, bool visible)
{
window.IsVisible = visible;
SendCmd(new CmdWinSetVisible { Window = WinPtr(window), Visible = visible });
}
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL.SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
if (cmd.Visible)
SDL.SDL_ShowWindow(cmd.Window);
else
SDL.SDL_HideWindow(cmd.Window);
}
public void WindowRequestAttention(WindowReg window)
{
SendCmd(new CmdWinRequestAttention { Window = WinPtr(window) });
}
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
{
var res = SDL.SDL_FlashWindow(cmd.Window, SDL.SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED);
if (!res)
_sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
}
public unsafe void WindowSwapBuffers(WindowReg window)
{
var reg = (Sdl3WindowReg)window;
var windowPtr = WinPtr(reg);
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
//
// Windows DwmFlush logic partly taken from:
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
// See also: https://github.com/libsdl-org/SDL/issues/5797
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
{
var curCtx = SDL.SDL_GL_GetCurrentContext();
var curWin = SDL.SDL_GL_GetCurrentWindow();
if (curCtx != reg.GlContext || curWin != reg.Sdl3Window)
throw new InvalidOperationException("Window context must be current!");
SDL.SDL_GL_SetSwapInterval(0);
dwmFlush = true;
swapInterval = reg.SwapInterval;
}
}
SDL.SDL_GL_SwapWindow(windowPtr);
if (dwmFlush)
{
var i = swapInterval;
while (i-- > 0)
{
Windows.DwmFlush();
}
SDL.SDL_GL_SetSwapInterval(swapInterval);
}
}
public uint? WindowGetX11Id(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.X11)
return null;
var reg = (Sdl3WindowReg)window;
return reg.X11Id;
}
public nint? WindowGetX11Display(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.X11)
return null;
var reg = (Sdl3WindowReg)window;
return reg.X11Display;
}
public nint? WindowGetWin32Window(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.Windows)
return null;
var reg = (Sdl3WindowReg)window;
return reg.WindowsHwnd;
}
public void RunOnWindowThread(Action a)
{
SendCmd(new CmdRunAction { Action = a });
}
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
SendCmd(new CmdTextInputSetRect
{
Window = WinPtr(reg),
Rect = new SDL.SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
},
Cursor = cursor
});
}
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
{
var rect = cmdTextInput.Rect;
SDL.SDL_SetTextInputArea(cmdTextInput.Window, ref rect, cmdTextInput.Cursor);
}
public void TextInputStart(WindowReg reg)
{
SendCmd(new CmdTextInputStart { Window = WinPtr(reg) });
}
private static void WinThreadStartTextInput(CmdTextInputStart cmd)
{
SDL.SDL_StartTextInput(cmd.Window);
}
public void TextInputStop(WindowReg reg)
{
SendCmd(new CmdTextInputStop { Window = WinPtr(reg) });
}
private static void WinThreadStopTextInput(CmdTextInputStop cmd)
{
SDL.SDL_StopTextInput(cmd.Window);
}
public void ClipboardSetText(WindowReg mainWindow, string text)
{
SendCmd(new CmdSetClipboard { Text = text });
}
private void WinThreadSetClipboard(CmdSetClipboard cmd)
{
var res = SDL.SDL_SetClipboardText(cmd.Text);
if (res)
_sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError());
}
public Task<string> ClipboardGetText(WindowReg mainWindow)
{
var tcs = new TaskCompletionSource<string>();
SendCmd(new CmdGetClipboard { Tcs = tcs });
return tcs.Task;
}
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
{
cmd.Tcs.TrySetResult(SDL.SDL_GetClipboardText());
}
private static void CheckWindowDisposed(WindowReg reg)
{
if (reg.IsDisposed)
throw new ObjectDisposedException("Window disposed");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nint WinPtr(WindowReg reg) => ((Sdl3WindowReg)reg).Sdl3Window;
private WindowReg? FindWindow(uint windowId)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (Sdl3WindowReg)windowReg;
if (glfwReg.WindowId == windowId)
return windowReg;
}
return null;
}
private sealed class Sdl3WindowReg : WindowReg
{
public nint Sdl3Window;
public uint WindowId;
public nint GlContext;
#pragma warning disable CS0649
public bool Fullscreen;
#pragma warning restore CS0649
public int SwapInterval;
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
public nint WindowsHwnd;
public nint X11Display;
public uint X11Id;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Robust.Client.Utility;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed unsafe partial class Sdl3WindowingImpl
{
// Experimentally on my system, SM_CXICON is 32.
// I doubt MS is ever changing that, so...
// I wish SDL would take care of this instead of us having to figure out what the "main" icon is. Ugh.
private const int MainWindowIconSize = 32;
// Writing this out like this makes me realize we're spending multiple hundred KBs on storing the window icon.
// You know, come to think about it, what if we used LZ4 or Zstd to compress the window icon stored here?
// This is absolutely not worth the optimization but hilarious for me to think about.
// The surface used for the window icon.
// This may store additional surfaces as alternate forms.
private nint _windowIconSurface;
// The data for all the window icons surfaces.
// Must be kept around! Pinned!
// ReSharper disable once CollectionNeverQueried.Local
private byte[][]? _windowIconData;
private void LoadWindowIcons()
{
// Sort such that closest to 64 is first.
// SDL3 doesn't "figure it out itself" as much as GLFW does, which sucks.
var icons = _clyde.LoadWindowIcons().OrderBy(i => Math.Abs(i.Width - MainWindowIconSize)).ToArray();
if (icons.Length == 0)
{
// No window icons at all!
return;
}
_windowIconData = new byte[icons.Length][];
var mainImg = icons[0];
_sawmill.Verbose(
"Have {iconCount} window icons available, choosing {mainIconWidth}x{mainIconHeight} as main",
icons.Length,
mainImg.Width,
mainImg.Height);
(_windowIconSurface, var mainData) = CreateSurfaceFromImage(mainImg);
_windowIconData[0] = mainData;
for (var i = 1; i < icons.Length; i++)
{
var (surface, data) = CreateSurfaceFromImage(icons[i]);
_windowIconData[i] = data;
SDL.SDL_AddSurfaceAlternateImage(_windowIconSurface, surface);
// Kept alive by the main surface.
SDL.SDL_DestroySurface(surface);
}
return;
static (nint, byte[]) CreateSurfaceFromImage(Image<Rgba32> img)
{
var span = MemoryMarshal.AsBytes(img.GetPixelSpan());
var copied = GC.AllocateUninitializedArray<byte>(span.Length, pinned: true);
span.CopyTo(copied);
IntPtr surface;
fixed (byte* ptr = copied)
{
surface = SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
(IntPtr)ptr,
sizeof(Rgba32) * img.Width);
}
return (surface, copied);
}
}
private void DestroyWindowIcons()
{
SDL.SDL_DestroySurface(_windowIconSurface);
_windowIconSurface = 0;
_windowIconData = null;
}
private void AssignWindowIconToWindow(nint window)
{
if (_windowIconSurface == 0)
return;
SDL.SDL_SetWindowIcon(window, (nint) _windowIconSurface);
}
}
}

View File

@@ -4,15 +4,15 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using Robust.Shared;
using Robust.Shared.Maths;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static SDL2.SDL;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
private sealed partial class Sdl3WindowingImpl
{
private bool _windowingRunning;
private ChannelWriter<CmdBase> _cmdWriter = default!;
@@ -29,34 +29,34 @@ internal partial class Clyde
while (_windowingRunning)
{
var res = SDL_WaitEvent(out Unsafe.NullRef<SDL_Event>());
if (res == 0)
var res = SDL.SDL_WaitEvent(out Unsafe.NullRef<SDL.SDL_Event>());
if (!res)
{
_sawmill.Error("Error while waiting on SDL2 events: {error}", SDL_GetError());
_sawmill.Error("Error while waiting on SDL3 events: {error}", SDL.SDL_GetError());
continue; // Assume it's a transient failure?
}
while (SDL_PollEvent(out _) == 1)
while (SDL.SDL_PollEvent(out _))
{
// We let callbacks process all events because of stuff like resizing.
}
while (_cmdReader.TryRead(out var cmd) && _windowingRunning)
{
ProcessSdl2Cmd(cmd);
ProcessSdl3Cmd(cmd);
}
}
}
public void PollEvents()
{
while (SDL_PollEvent(out _) == 1)
while (SDL.SDL_PollEvent(out _))
{
// We let callbacks process all events because of stuff like resizing.
}
}
private void ProcessSdl2Cmd(CmdBase cmdb)
private void ProcessSdl3Cmd(CmdBase cmdb)
{
switch (cmdb)
{
@@ -113,20 +113,24 @@ internal partial class Clyde
WinThreadWinCursorSet(cmd);
break;
case CmdWinWinSetMode cmd:
WinThreadWinSetMode(cmd);
case CmdWinWinSetFullscreen cmd:
WinThreadWinSetFullscreen(cmd);
break;
case CmdWinSetWindowed cmd:
WinThreadWinSetWindowed(cmd);
break;
case CmdTextInputSetRect cmd:
WinThreadSetTextInputRect(cmd);
break;
case CmdTextInputStart:
WinThreadStartTextInput();
case CmdTextInputStart cmd:
WinThreadStartTextInput(cmd);
break;
case CmdTextInputStop:
WinThreadStopTextInput();
case CmdTextInputStop cmd:
WinThreadStopTextInput(cmd);
break;
}
}
@@ -171,25 +175,25 @@ internal partial class Clyde
_eventWriter = eventChannel.Writer;
}
private unsafe void SendCmd(CmdBase cmd)
private void SendCmd(CmdBase cmd)
{
if (_clyde._threadWindowApi)
{
_cmdWriter.TryWrite(cmd);
SDL_Event ev = default;
ev.type = (SDL_EventType)_sdlEventWakeup;
SDL.SDL_Event ev = default;
ev.type = _sdlEventWakeup;
// Post empty event to unstuck WaitEvents if necessary.
// This self-registered event type is ignored by the winthread, but it'll still wake it up.
// This can fail if the event queue is full.
// That's not really a problem since in that case something else will be sure to wake the thread up anyways.
// NOTE: have to avoid using PushEvents since that invokes callbacks which causes a deadlock.
SDL_PeepEvents(&ev, 1, SDL_eventaction.SDL_ADDEVENT, ev.type, ev.type);
SDL.SDL_PeepEvents(new Span<SDL.SDL_Event>(ref ev), 1, SDL.SDL_EventAction.SDL_ADDEVENT, ev.type, ev.type);
}
else
{
ProcessSdl2Cmd(cmd);
ProcessSdl3Cmd(cmd);
}
}
@@ -211,93 +215,119 @@ internal partial class Clyde
}
private abstract record CmdBase;
private abstract class CmdBase;
private sealed record CmdTerminate : CmdBase;
private sealed class CmdTerminate : CmdBase;
private sealed record CmdWinCreate(
GLContextSpec? GLSpec,
WindowCreateParameters Parameters,
nint ShareWindow,
nint ShareContext,
nint OwnerWindow,
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
) : CmdBase;
private sealed class CmdWinCreate : CmdBase
{
public required GLContextSpec? GLSpec;
public required WindowCreateParameters Parameters;
public required nint ShareWindow;
public required nint ShareContext;
public required nint OwnerWindow;
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
}
private sealed record CmdWinDestroy(
nint Window,
bool HadOwner
) : CmdBase;
private sealed class CmdWinDestroy : CmdBase
{
public nint Window;
public bool HadOwner;
}
private sealed record Sdl2WindowCreateResult(
Sdl2WindowReg? Reg,
string? Error
);
private sealed class Sdl3WindowCreateResult
{
public Sdl3WindowReg? Reg;
public string? Error;
}
private sealed record CmdRunAction(
Action Action
) : CmdBase;
private sealed class CmdRunAction : CmdBase
{
public required Action Action;
}
private sealed record CmdSetClipboard(
string Text
) : CmdBase;
private sealed class CmdSetClipboard : CmdBase
{
public required string Text;
}
private sealed record CmdGetClipboard(
TaskCompletionSource<string> Tcs
) : CmdBase;
private sealed class CmdGetClipboard : CmdBase
{
public required TaskCompletionSource<string> Tcs;
}
private sealed record CmdWinRequestAttention(
nint Window
) : CmdBase;
private sealed class CmdWinRequestAttention : CmdBase
{
public nint Window;
}
private sealed record CmdWinSetSize(
nint Window,
int W, int H
) : CmdBase;
private sealed class CmdWinSetSize : CmdBase
{
public nint Window;
public int W;
public int H;
}
private sealed record CmdWinSetVisible(
nint Window,
bool Visible
) : CmdBase;
private sealed class CmdWinSetVisible : CmdBase
{
public nint Window;
public bool Visible;
}
private sealed record CmdWinSetTitle(
nint Window,
string Title
) : CmdBase;
private sealed class CmdWinSetTitle : CmdBase
{
public nint Window;
public required string Title;
}
private sealed record CmdCursorCreate(
Image<Rgba32> Bytes,
Vector2i Hotspot,
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdCursorCreate : CmdBase
{
public required Image<Rgba32> Bytes;
public Vector2i Hotspot;
public ClydeHandle Cursor;
}
private sealed record CmdCursorDestroy(
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdCursorDestroy : CmdBase
{
public ClydeHandle Cursor;
}
private sealed record CmdWinCursorSet(
nint Window,
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdWinCursorSet : CmdBase
{
public nint Window;
public ClydeHandle Cursor;
}
private sealed record CmdWinWinSetMode(
nint Window,
WindowMode Mode
) : CmdBase;
private sealed class CmdWinWinSetFullscreen : CmdBase
{
public nint Window;
}
private sealed class CmdWinSetWindowed : CmdBase
{
public nint Window;
public int Width;
public int Height;
public int PosX;
public int PosY;
}
// IME
private sealed record CmdTextInputStart : CmdBase
private sealed class CmdTextInputStart : CmdBase
{
public static readonly CmdTextInputStart Instance = new();
public nint Window;
}
private sealed record CmdTextInputStop : CmdBase
private sealed class CmdTextInputStop : CmdBase
{
public static readonly CmdTextInputStop Instance = new();
public nint Window;
}
private sealed record CmdTextInputSetRect(
SDL_Rect Rect
) : CmdBase;
private sealed class CmdTextInputSetRect : CmdBase
{
public nint Window;
public SDL.SDL_Rect Rect;
public int Cursor;
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using SDL3;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IWindowingImpl
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly Clyde _clyde;
private GCHandle _selfGCHandle;
private readonly ISawmill _sawmill;
private readonly ISawmill _sawmillSdl3;
private SdlVideoDriver _videoDriver;
public Sdl3WindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;
deps.InjectDependencies(this, true);
_sawmill = _logManager.GetSawmill("clyde.win");
_sawmillSdl3 = _logManager.GetSawmill("clyde.win.sdl3");
}
public bool Init()
{
InitChannels();
if (!InitSdl3())
return false;
return true;
}
private unsafe bool InitSdl3()
{
CheckThreadApartment();
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
SDL.SDL_SetLogPriorities(SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
SDL.SDL_SetLogOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL.SDL_SetHint(SDL.SDL_HINT_IME_IMPLEMENTED_UI, "composition");
// SDL3's GameInput support is currently broken and leaving it on
// causes a "that operation is not supported" error to be logged on startup.
// https://github.com/libsdl-org/SDL/issues/11813
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
if (!res)
{
_sawmill.Fatal("Failed to initialize SDL3: {error}", SDL.SDL_GetError());
return false;
}
var version = SDL.SDL_GetVersion();
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
_sawmill.Debug(
"SDL3 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}",
SDL.SDL_VERSIONNUM_MAJOR(version),
SDL.SDL_VERSIONNUM_MINOR(version),
SDL.SDL_VERSIONNUM_MICRO(version),
videoDriver);
LoadSdl3VideoDriver();
_sdlEventWakeup = SDL.SDL_RegisterEvents(1);
if (_sdlEventWakeup == 0)
throw new InvalidOperationException("SDL_RegisterEvents failed");
LoadWindowIcons();
InitCursors();
InitMonitors();
ReloadKeyMap();
SDL.SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
return true;
}
private void CheckThreadApartment()
{
if (!OperatingSystem.IsWindows())
return;
if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
_sawmill.Error("Thread apartment state isn't STA. This will likely break things!!!");
}
private void LoadSdl3VideoDriver()
{
_videoDriver = SDL.SDL_GetCurrentVideoDriver() switch
{
"windows" => SdlVideoDriver.Windows,
"x11" => SdlVideoDriver.X11,
_ => SdlVideoDriver.Other,
};
}
public unsafe void Shutdown()
{
if (_selfGCHandle != default)
{
SDL.SDL_RemoveEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
_selfGCHandle.Free();
_selfGCHandle = default;
}
SDL.SDL_SetLogOutputFunction(null, null);
if (SDL.SDL_WasInit(0) != 0)
{
_sawmill.Debug("Terminating SDL3");
SDL.SDL_Quit();
}
}
public void FlushDispose()
{
// Not currently used
}
public void GLMakeContextCurrent(WindowReg? reg)
{
SDL.SDLBool res;
if (reg is Sdl3WindowReg sdlReg)
res = SDL.SDL_GL_MakeCurrent(sdlReg.Sdl3Window, sdlReg.GlContext);
else
res = SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (!res)
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL.SDL_GetError());
}
public void GLSwapInterval(WindowReg reg, int interval)
{
((Sdl3WindowReg)reg).SwapInterval = interval;
SDL.SDL_GL_SetSwapInterval(interval);
}
public unsafe void* GLGetProcAddress(string procName)
{
return (void*) SDL.SDL_GL_GetProcAddress(procName);
}
public string GetDescription()
{
var version = SDL.SDL_GetVersion();
var major = SDL.SDL_VERSIONNUM_MAJOR(version);
var minor = SDL.SDL_VERSIONNUM_MINOR(version);
var micro = SDL.SDL_VERSIONNUM_MICRO(version);
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
var revision = SDL.SDL_GetRevision();
return $"SDL {major}.{minor}.{micro} (rev: {revision}, video driver: {videoDriver})";
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void LogOutputFunction(
void* userdata,
int category,
SDL.SDL_LogPriority priority,
byte* message)
{
var obj = (Sdl3WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
var level = priority switch
{
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
_ => LogLevel.Error
};
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
var categoryName = SdlLogCategoryName(category);
obj._sawmillSdl3.Log(level, $"[{categoryName}] {msg}");
}
private static string SdlLogCategoryName(int category)
{
return (SDL.SDL_LogCategory) category switch {
// @formatter:off
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION => "application",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ERROR => "error",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ASSERT => "assert",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_SYSTEM => "system",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_AUDIO => "audio",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_VIDEO => "video",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_RENDER => "render",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_INPUT => "input",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_TEST => "test",
_ => "unknown"
// @formatter:on
};
}
private enum SdlVideoDriver
{
// These are the ones we need to be able to check against.
Other,
Windows,
X11
}
}
}

View File

@@ -2,7 +2,10 @@
using System.Runtime.InteropServices;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
using X11Window = TerraFX.Interop.Xlib.Window;
namespace Robust.Client.Graphics.Clyde;
@@ -33,6 +36,8 @@ internal partial class Clyde
public static unsafe void WindowsSharedWindowCreate(HWND hWnd, IConfigurationManager cfg)
{
// TODO: REMOVE, only used by GLFW, SDL3 does DWMWA_USE_IMMERSIVE_DARK_MODE automatically.
// >= Windows 11 22000 check
if (cfg.GetCVar(CVars.DisplayWin11ImmersiveDarkMode) && Environment.OSVersion.Version.Build >= 22000)
{
@@ -40,5 +45,37 @@ internal partial class Clyde
Windows.DwmSetWindowAttribute(hWnd, 20, &b, (uint) sizeof(BOOL));
}
}
public static void SetWindowStyleNoTitleOptionsWindows(HWND hWnd)
{
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWL.GWL_STYLE,
// Cast to long here to work around a bug in rider with nint bitwise operators.
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
}
public static unsafe void SetWindowStyleNoTitleOptionsX11(Display* x11Display, X11Window x11Window)
{
DebugTools.Assert(x11Window != X11Window.NULL);
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
DebugTools.Assert(newPropVal != Atom.NULL);
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
#pragma warning disable CA1806
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
Xlib.XChangeProperty(x11Display, x11Window,
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
(byte*)&newPropVal, 1);
#pragma warning restore CA1806
Marshal.FreeCoTaskMem(newPropValString);
Marshal.FreeCoTaskMem(propNameString);
}
}
}

View File

@@ -133,29 +133,5 @@ namespace Robust.Client.Graphics
IEnumerable<IClydeMonitor> EnumerateMonitors();
IClydeWindow CreateWindow(WindowCreateParameters parameters);
/// <summary>
/// Set the active text input area in window pixel coordinates.
/// </summary>
/// <param name="rect">
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
/// </param>
void TextInputSetRect(UIBox2i rect);
/// <summary>
/// Indicate that the game should start accepting text input on the currently focused window.
/// </summary>
/// <remarks>
/// On some platforms, this will cause an on-screen keyboard to appear.
/// The game will also start accepting IME input if configured by the user.
/// </remarks>
/// <seealso cref="TextInputStop"/>
void TextInputStart();
/// <summary>
/// Stop text input, opposite of <see cref="TextInputStart"/>.
/// </summary>
/// <seealso cref="TextInputStart"/>
void TextInputStop();
}
}

View File

@@ -69,5 +69,7 @@ namespace Robust.Client.Graphics
void ShutdownGridEcsEvents();
void RunOnWindowThread(Action action);
IFileDialogManager? FileDialogImpl { get; }
}
}

View File

@@ -41,6 +41,30 @@ namespace Robust.Client.Graphics
/// Raised when the window has been resized.
/// </summary>
event Action<WindowResizedEventArgs> Resized;
/// <summary>
/// Set the active text input area in window pixel coordinates.
/// </summary>
/// <param name="rect">
/// This information is used by the OS to position overlays like IMEs or emoji pickers etc.
/// </param>
void TextInputSetRect(UIBox2i rect, int cursor);
/// <summary>
/// Indicate that the game should start accepting text input on the currently focused window.
/// </summary>
/// <remarks>
/// On some platforms, this will cause an on-screen keyboard to appear.
/// The game will also start accepting IME input if configured by the user.
/// </remarks>
/// <seealso cref="TextInputStop"/>
void TextInputStart();
/// <summary>
/// Stop text input, opposite of <see cref="TextInputStart"/>.
/// </summary>
/// <seealso cref="TextInputStart"/>
void TextInputStop();
}
public interface IClydeWindowInternal : IClydeWindow

View File

@@ -880,7 +880,7 @@ namespace Robust.Client.UserInterface.Controls
if (Editable)
{
_clyde.TextInputStart();
Root?.Window?.TextInputStart();
}
_focusedOnFrame = _timing.CurFrame;
@@ -897,7 +897,8 @@ namespace Robust.Client.UserInterface.Controls
OnFocusExit?.Invoke(new LineEditEventArgs(this, _text));
_clyde.TextInputStop();
Root?.Window?.TextInputStop();
AbortIme(delete: false);
}
@@ -1145,15 +1146,16 @@ namespace Robust.Client.UserInterface.Controls
contentBox.Bottom),
cursorColor);
if (Root?.Window is { } window)
{
// Update IME position.
var imeBox = new UIBox2(
actualCursorPosition,
contentBox.Left,
contentBox.Top,
contentBox.Right,
contentBox.Bottom);
_master._clyde.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition));
window.TextInputSetRect((UIBox2i) imeBox.Translated(GlobalPixelPosition), actualCursorPosition);
}
}

View File

@@ -1366,15 +1366,16 @@ public sealed class TextEdit : Control
baseLine.Y + descent),
cursorColor);
if (UserInterfaceManager.KeyboardFocused == _master)
if (UserInterfaceManager.KeyboardFocused == _master && Root?.Window is { } window)
{
var box = (UIBox2i)new UIBox2(
baseLine.X,
drawBox.Left,
baseLine.Y - height + descent,
drawBox.Right,
baseLine.Y + descent);
var cursorOffset = baseLine.X - drawBox.Left;
_master._clyde.TextInputSetRect(box.Translated(GlobalPixelPosition));
window.TextInputSetRect(box.Translated(GlobalPixelPosition), (int) cursorOffset);
}
}
@@ -1446,7 +1447,7 @@ public sealed class TextEdit : Control
if (Editable)
{
_clyde.TextInputStart();
Root?.Window?.TextInputStart();
}
}
@@ -1454,7 +1455,7 @@ public sealed class TextEdit : Control
{
base.KeyboardFocusExited();
_clyde.TextInputStop();
Root?.Window?.TextInputStop();
AbortIme(delete: false);
}

View File

@@ -32,6 +32,9 @@ namespace Robust.Client.UserInterface
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.OpenFile(filters);
var name = await GetOpenFileName(filters);
if (name == null)
{
@@ -53,6 +56,9 @@ namespace Robust.Client.UserInterface
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
{
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.SaveFile(filters);
var name = await GetSaveFileName(filters);
if (name == null)
{

View File

@@ -1197,6 +1197,7 @@ Types:
RuntimeFieldHandle: { }
RuntimeMethodHandle: { }
RuntimeTypeHandle: { }
STAThreadAttribute: { All: True }
SByte: { All: True }
Single: { All: True }
Span`1: