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 _cmdWriter = default!; private ChannelReader _cmdReader = default!; private ChannelReader _eventReader = default!; private ChannelWriter _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(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(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 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 Tcs ) : CmdBase; private sealed record CmdWinCursorSet( nint Window, ClydeHandle Cursor ) : CmdBase; private sealed record CmdCursorCreate( Image Bytes, Vector2i Hotspot, ClydeHandle Cursor ) : CmdBase; private sealed record CmdCursorDestroy( ClydeHandle Cursor ) : CmdBase; private sealed record CmdRunAction( Action Action ) : CmdBase; } } }