Files
RobustToolbox/Robust.Client/Graphics/Clyde/Windowing/Glfw.WindowThread.cs
wixoa 46143d2589 Separate window creation in OSWindow.Show() to allow creation in the background (#5489)
* OSWindow rework
OSWindow now created ClydeWindow and WindowRoot immediately, but non-visible in the background
Also added the ability to programatically resize an open window

* Implement window resizing on SDL2

* Revert OSWindow changes

* Split `Show()` into `Create()` and `Show()`

* Formatting
2024-10-17 17:21:38 +02:00

304 lines
9.6 KiB
C#

using System;
using System.Threading.Channels;
using System.Threading.Tasks;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Shared;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed partial class GlfwWindowingImpl
{
private bool _windowingRunning;
private ChannelWriter<CmdBase> _cmdWriter = default!;
private ChannelReader<CmdBase> _cmdReader = default!;
private ChannelReader<EventBase> _eventReader = default!;
private ChannelWriter<EventBase> _eventWriter = default!;
//
// Let it be forever recorded that I started work on windowing thread separation
// because win32 SetCursor was taking 15ms spinwaiting inside the kernel.
//
//
// To avoid stutters and solve some other problems like smooth window resizing,
// we (by default) use a separate thread for windowing.
//
// Types like WindowReg are considered to be part of the "game" thread
// and should **NOT** be directly updated/accessed from the windowing thread.
//
// Got that?
//
//
// The windowing -> game channel is bounded so that the OS properly detects the game as locked
// up when it actually locks up. The other way around is not bounded to avoid deadlocks.
// This also means that all operations like clipboard reading, window creation, etc....
// have to be asynchronous.
//
public void EnterWindowLoop()
{
_windowingRunning = true;
while (_windowingRunning)
{
// glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread
// (despite what the docs claim, and yes this makes it useless).
// Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead.
if (OperatingSystem.IsMacOS())
GLFW.WaitEventsTimeout(0.008);
else
GLFW.WaitEvents();
while (_cmdReader.TryRead(out var cmd) && _windowingRunning)
{
ProcessGlfwCmd(cmd);
}
}
}
public void PollEvents()
{
GLFW.PollEvents();
}
private void ProcessGlfwCmd(CmdBase cmdb)
{
switch (cmdb)
{
case CmdTerminate:
_windowingRunning = false;
_eventWriter.Complete();
break;
case CmdWinSetTitle cmd:
WinThreadWinSetTitle(cmd);
break;
case CmdWinSetMonitor cmd:
WinThreadWinSetMonitor(cmd);
break;
case CmdWinSetSize cmd:
WinThreadWinSetSize(cmd);
break;
case CmdWinSetVisible cmd:
WinThreadWinSetVisible(cmd);
break;
case CmdWinRequestAttention cmd:
WinThreadWinRequestAttention(cmd);
break;
case CmdWinSetFullscreen cmd:
WinThreadWinSetFullscreen(cmd);
break;
case CmdWinCreate cmd:
WinThreadWinCreate(cmd);
break;
case CmdWinDestroy cmd:
WinThreadWinDestroy(cmd);
break;
case CmdSetClipboard cmd:
WinThreadSetClipboard(cmd);
break;
case CmdGetClipboard cmd:
WinThreadGetClipboard(cmd);
break;
case CmdCursorCreate cmd:
WinThreadCursorCreate(cmd);
break;
case CmdCursorDestroy cmd:
WinThreadCursorDestroy(cmd);
break;
case CmdWinCursorSet cmd:
WinThreadWinCursorSet(cmd);
break;
case CmdRunAction cmd:
cmd.Action();
break;
}
}
public void TerminateWindowLoop()
{
SendCmd(new CmdTerminate());
_cmdWriter.Complete();
// Drain command queue ignoring it until the window thread confirms completion.
#pragma warning disable RA0004
while (_eventReader.WaitToReadAsync().AsTask().Result)
#pragma warning restore RA0004
{
_eventReader.TryRead(out _);
}
}
private void InitChannels()
{
var cmdChannel = Channel.CreateUnbounded<CmdBase>(new UnboundedChannelOptions
{
SingleReader = true,
// Finalizers can write to this in some cases.
SingleWriter = false
});
_cmdReader = cmdChannel.Reader;
_cmdWriter = cmdChannel.Writer;
var bufferSize = _cfg.GetCVar(CVars.DisplayInputBufferSize);
var eventChannel = Channel.CreateBounded<EventBase>(new BoundedChannelOptions(bufferSize)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true,
// For unblocking continuations.
AllowSynchronousContinuations = true
});
_eventReader = eventChannel.Reader;
_eventWriter = eventChannel.Writer;
}
private void SendCmd(CmdBase cmd)
{
if (_clyde._threadWindowApi)
{
_cmdWriter.TryWrite(cmd);
// Post empty event to unstuck WaitEvents if necessary.
if (!OperatingSystem.IsMacOS())
GLFW.PostEmptyEvent();
}
else
{
ProcessGlfwCmd(cmd);
}
}
private void SendEvent(EventBase ev)
{
if (_clyde._threadWindowApi)
{
var task = _eventWriter.WriteAsync(ev);
if (!task.IsCompletedSuccessfully)
{
task.AsTask().Wait();
}
}
else
{
ProcessEvent(ev);
}
}
public void RunOnWindowThread(Action action)
{
SendCmd(new CmdRunAction(action));
}
private abstract record CmdBase;
private sealed record CmdTerminate : CmdBase;
private sealed record CmdWinSetTitle(
nint Window,
string Title
) : CmdBase;
private sealed record CmdWinSetMonitor(
nint Window,
int MonitorId,
int X, int Y,
int W, int H,
int RefreshRate
) : CmdBase;
private sealed record CmdWinMaximize(
nint Window
) : CmdBase;
private sealed record CmdWinSetFullscreen(
nint Window
) : CmdBase;
private sealed record CmdWinSetSize(
nint Window,
int W, int H
) : CmdBase;
private sealed record CmdWinSetVisible(
nint Window,
bool Visible
) : CmdBase;
private sealed record CmdWinRequestAttention(
nint Window
) : CmdBase;
private sealed record CmdWinCreate(
GLContextSpec? GLSpec,
WindowCreateParameters Parameters,
nint ShareWindow,
nint OwnerWindow,
TaskCompletionSource<GlfwWindowCreateResult> Tcs
) : CmdBase;
private sealed record CmdWinDestroy(
nint Window,
bool hadOwner
) : CmdBase;
private sealed record GlfwWindowCreateResult(
GlfwWindowReg? Reg,
(string Desc, ErrorCode Code)? Error
);
private sealed record CmdSetClipboard(
nint Window,
string Text
) : CmdBase;
private sealed record CmdGetClipboard(
nint Window,
TaskCompletionSource<string> Tcs
) : CmdBase;
private sealed record CmdWinCursorSet(
nint Window,
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdCursorCreate(
Image<Rgba32> Bytes,
Vector2i Hotspot,
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdCursorDestroy(
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdRunAction(
Action Action
) : CmdBase;
}
}
}