diff --git a/.gitignore b/.gitignore
index 4ac3ded..5e21b14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -644,6 +644,7 @@ nohup.out
###########################
# CatalystUI Negotiations #
###########################
+!**/*/Examples/**/PublishProfiles/PublishExamples.pubxml
!**/*.Profiling/
!**/*.Profiling/*.Profiling.csproj
!**/*.Profiling/Program.cs
diff --git a/.scripts/Setup.ps1 b/.scripts/Setup.ps1
index 999152a..41e0c1e 100644
--- a/.scripts/Setup.ps1
+++ b/.scripts/Setup.ps1
@@ -73,7 +73,7 @@ $projectsList = @(
Depends = @("Arcane")
},
@{
- Module = "Crystal"
+ Module = "Crystal.Core"
Projects = @(
@{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Core" }
)
@@ -86,7 +86,39 @@ $projectsList = @(
@{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Glfw3" }
)
PromptIgnore = $false
- Depends = @("Crystal")
+ Depends = @("Crystal.Core")
+ },
+ @{
+ Module = "Crystal.Sdl2"
+ Projects = @(
+ @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Sdl2" }
+ )
+ PromptIgnore = $false
+ Depends = @("Crystal.Core")
+ },
+ @{
+ Module = "Crystal.OpenGL"
+ Projects = @(
+ @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.OpenGL" }
+ )
+ PromptIgnore = $false
+ Depends = @("Crystal.Core")
+ },
+ @{
+ Module = "Crystal.Glfw3OpenGLSurfaceConnector"
+ Projects = @(
+ @{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector" }
+ )
+ PromptIgnore = $false
+ Depends = @("Crystal.Glfw3", "Crystal.OpenGL")
+ },
+ @{
+ Module = "Catalyst.Examples.BasicWindow"
+ Projects = @(
+ @{ Folder = "Examples/BasicWindow"; Name = "CatalystUI.Examples.BasicWindow" }
+ )
+ PromptIgnore = $false
+ Depends = @("Crystal.Glfw3", "Crystal.OpenGL", "Crystal.Glfw3OpenGLSurfaceConnector" )
}
)
diff --git a/CatalystUI/CatalystUI.sln b/CatalystUI/CatalystUI.sln
index 6a5b756..2444580 100644
--- a/CatalystUI/CatalystUI.sln
+++ b/CatalystUI/CatalystUI.sln
@@ -40,6 +40,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.Glfw3", "Modules\Crystal\CatalystUI.Modules.Crystal.Glfw3\CatalystUI.Modules.Crystal.Glfw3.csproj", "{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.Sdl2", "Modules\Crystal\CatalystUI.Modules.Crystal.Sdl2\CatalystUI.Modules.Crystal.Sdl2.csproj", "{532C59A0-43D2-44E9-B6ED-E88E6B8770F5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.OpenGL", "Modules\Crystal\CatalystUI.Modules.Crystal.OpenGL\CatalystUI.Modules.Crystal.OpenGL.csproj", "{9CD307E2-FEDA-4C64-B2EE-00E6E196175C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector", "Modules\Crystal\CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector\CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector.csproj", "{B03716DE-8B0C-4457-B71A-5E0A0D11325D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{4A4866FE-D5FE-4B0D-B5FB-A6C9090A137D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalystUI.Examples.BasicWindow", "Examples\CatalystUI.Examples.BasicWindow\CatalystUI.Examples.BasicWindow.csproj", "{CDBADF64-0FE0-4911-BDF8-07C4DB887337}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +112,22 @@ Global
{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18}.Release|Any CPU.Build.0 = Release|Any CPU
+ {532C59A0-43D2-44E9-B6ED-E88E6B8770F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {532C59A0-43D2-44E9-B6ED-E88E6B8770F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {532C59A0-43D2-44E9-B6ED-E88E6B8770F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {532C59A0-43D2-44E9-B6ED-E88E6B8770F5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CD307E2-FEDA-4C64-B2EE-00E6E196175C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CD307E2-FEDA-4C64-B2EE-00E6E196175C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CD307E2-FEDA-4C64-B2EE-00E6E196175C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CD307E2-FEDA-4C64-B2EE-00E6E196175C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B03716DE-8B0C-4457-B71A-5E0A0D11325D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B03716DE-8B0C-4457-B71A-5E0A0D11325D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B03716DE-8B0C-4457-B71A-5E0A0D11325D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B03716DE-8B0C-4457-B71A-5E0A0D11325D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CDBADF64-0FE0-4911-BDF8-07C4DB887337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CDBADF64-0FE0-4911-BDF8-07C4DB887337}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CDBADF64-0FE0-4911-BDF8-07C4DB887337}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CDBADF64-0FE0-4911-BDF8-07C4DB887337}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{68F496AC-9438-40F1-9DF8-97363033D661} = {7EC51871-49A8-4991-BDAF-F43A4E9B8C9D}
@@ -121,5 +147,9 @@ Global
{047744B0-87DC-4808-99C4-5AC1F8A1EB4D} = {C8B02B42-826B-4EDE-B72F-F4F97C1A088D}
{4DE1A1EB-103B-4E07-859A-354908ACE0E2} = {41BEF490-7005-4D10-9958-22D636F9DE38}
{BC5802D1-4C3E-4FD6-9E3B-9ED82CCB2D18} = {41BEF490-7005-4D10-9958-22D636F9DE38}
+ {532C59A0-43D2-44E9-B6ED-E88E6B8770F5} = {41BEF490-7005-4D10-9958-22D636F9DE38}
+ {9CD307E2-FEDA-4C64-B2EE-00E6E196175C} = {41BEF490-7005-4D10-9958-22D636F9DE38}
+ {B03716DE-8B0C-4457-B71A-5E0A0D11325D} = {41BEF490-7005-4D10-9958-22D636F9DE38}
+ {CDBADF64-0FE0-4911-BDF8-07C4DB887337} = {4A4866FE-D5FE-4B0D-B5FB-A6C9090A137D}
EndGlobalSection
EndGlobal
diff --git a/CatalystUI/CatalystUI.sln.DotSettings b/CatalystUI/CatalystUI.sln.DotSettings
new file mode 100644
index 0000000..d56d2c4
--- /dev/null
+++ b/CatalystUI/CatalystUI.sln.DotSettings
@@ -0,0 +1,2 @@
+
+ GL
\ No newline at end of file
diff --git a/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs b/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
index ae5d624..a3e9a66 100644
--- a/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
+++ b/CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
@@ -13,6 +13,7 @@
using Catalyst.Debugging;
using Catalyst.Threading;
using System;
+using System.Runtime.ExceptionServices;
using System.Threading;
namespace Catalyst {
@@ -67,6 +68,7 @@ internal CatalystApp(Func? runMethod = null) {
// Properties
_debug.Log(LogLevel.Verbose, "Capturing main thread dispatcher and running application...");
+ ExceptionDispatchInfo? exception = null;
ThreadDelegateDispatcher.Capture(dispatcher => {
// Assign the main-thread dispatcher to the app.
Dispatcher = dispatcher;
@@ -77,13 +79,16 @@ internal CatalystApp(Func? runMethod = null) {
if (runMethod != null) {
_debug.Log(LogLevel.Info, "CatalystApp initialized, initializing worker thread. Application will run until the provided run method exits.");
ThreadDelegateDispatcher workerThread = ThreadDelegateDispatcher.New("CallerThread");
- workerThread.Execute(() => {
- Exit(runMethod(this));
- });
+ workerThread.DelegateException += (_, _, e) => {
+ exception = e;
+ dispatcher.Dispose();
+ };
+ workerThread.Execute(() => Exit(runMethod(this)));
} else {
_debug.Log(LogLevel.Info, "CatalystApp initialized. No run method provided, application will run until exit.");
}
}, isMainThread: true);
+ exception?.Throw();
// Set the exit code and let the application return gracefully.
Environment.ExitCode = _exitCode;
diff --git a/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
index f7ba7b0..4dd3ef2 100644
--- a/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
+++ b/CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
@@ -28,38 +28,35 @@ public sealed class DelegateQueue : IDisposable {
public delegate void DelegateQueueEventHandler(DelegateQueue queue, EnqueuedDelegate @delegate);
///
- /// Invoked when a delegate is enqueued but prior to its execution.
+ /// An event handler delegate for exception events related to the .
+ ///
+ public delegate void DelegateQueueExceptionEventHandler(DelegateQueue queue, EnqueuedDelegate @delegate, ExceptionDispatchInfo? exception);
+
+ ///
+ /// Invoked when a delegate is enqueued.
///
- ///
- /// Invoked on the calling thread, not the executing thread.
- ///
- /// During invocation, the queue lock is held, so avoid long-running
- /// operations or deadlocks may occur.
- ///
public event DelegateQueueEventHandler? DelegateEnqueued;
///
- /// Invoked when a delegate is dequeued for execution but prior to its execution.
+ /// Invoked when a delegate is dequeued prior to execution.
///
- ///
- /// Invoked on the executing thread, not the calling thread.
- ///
- /// During invocation, the queue lock is held, so avoid long-running
- /// operations or deadlocks may occur.
- ///
public event DelegateQueueEventHandler? DelegateDequeued;
///
- /// Invoked after a delegate has been executed.
+ /// Invoked when a delegate has been executed successfully.
///
- ///
- /// Invoked on the executing thread, not the calling thread.
- ///
- /// During invocation, the queue lock is held, so avoid long-running
- /// operations or deadlocks may occur.
- ///
public event DelegateQueueEventHandler? DelegateExecuted;
+ ///
+ /// Invoked when a delegate throws an exception during execution.
+ ///
+ public event DelegateQueueExceptionEventHandler? DelegateException;
+
+ ///
+ /// Invoked when a delegate has finished execution, regardless of success or failure.
+ ///
+ public event DelegateQueueEventHandler? DelegateFinished;
+
///
/// The queue which is used to store enqueued delegates.
///
@@ -76,6 +73,11 @@ public sealed class DelegateQueue : IDisposable {
///
private readonly ManualResetEvent _queueFullWaitHandle;
+ ///
+ /// A wait handle that is signaled when a delegate is enqueued.
+ ///
+ private readonly ManualResetEvent _delegateEnqueuedWaitHandle;
+
///
/// The ID of the thread currently executing the queue.
///
@@ -101,6 +103,7 @@ public DelegateQueue(int? size = null) {
// Fields
_queue = new(size.Value);
_queueFullWaitHandle = new(false);
+ _delegateEnqueuedWaitHandle = new(true);
_executingThreadId = -1;
_disposed = false;
_lock = new();
@@ -166,6 +169,7 @@ public bool Enqueue(Delegate @delegate, nint caller, nint parameters, out nint @
}
entry.HasAwaiter = wait;
_queue.Enqueue();
+ _delegateEnqueuedWaitHandle.Set();
// Why would we not exit the lock here?
// Because we're still in the process of enqueuing,
@@ -189,7 +193,7 @@ public bool Enqueue(Delegate @delegate, nint caller, nint parameters, out nint @
entry.WaitHandle!.WaitOne(); // wait indefinitely
}
@return = entry.Return;
- if (entry.Exception != null) ExceptionDispatchInfo.Capture(entry.Exception).Throw();
+ entry.Exception?.Throw();
} else {
OnDelegateEnqueued(entry);
_lock.Exit(); // exit the lock
@@ -202,10 +206,16 @@ public bool Enqueue(Delegate @delegate, nint caller, nint parameters, out nint @
/// Loops through the queue and executes all enqueued delegates.
///
public void Execute() {
- ObjectDisposedException.ThrowIf(_disposed, this);
+ // Wait until there's at least one delegate to execute
+ _delegateEnqueuedWaitHandle.WaitOne();
+ if (_disposed) return; // if we've disposed of ourselves while waiting, exit immediately
+ _delegateEnqueuedWaitHandle.Reset();
+
+ // Enter the lock to begin execution
_lock.Enter();
try {
// If there's nothing to execute, return immediately
+ // (shouldn't be possible, but just in case)
if (_queue.IsEmpty) return;
// Loop through the queue and execute each delegate
@@ -214,11 +224,17 @@ public void Execute() {
ref EnqueuedDelegate entry = ref _queue.PeekDequeue();
OnDelegateDequeued(entry);
_lock.Exit(); // leave the lock for delegate execution
+ bool success = false;
try {
entry.Execute();
+ success = true;
+ } catch {
+ success = false;
} finally {
_lock.Enter(); // re-enter the lock after execution
- OnDelegateExecuted(entry);
+ if (success) OnDelegateExecuted(entry);
+ else OnDelegateException(entry, entry.Exception);
+ OnDelegateFinished(entry);
// if we disposed of ourselves or the queue while executing,
// it could have become empty, so we check again
if (!_disposed && !_queue.IsEmpty) {
@@ -252,6 +268,16 @@ private void OnDelegateExecuted(EnqueuedDelegate @delegate) {
DelegateExecuted?.Invoke(this, @delegate);
}
+ ///
+ private void OnDelegateException(EnqueuedDelegate @delegate, ExceptionDispatchInfo? exception) {
+ DelegateException?.Invoke(this, @delegate, exception);
+ }
+
+ ///
+ private void OnDelegateFinished(EnqueuedDelegate @delegate) {
+ DelegateFinished?.Invoke(this, @delegate);
+ }
+
///
/// Disposes of the .
///
@@ -267,9 +293,13 @@ private void Dispose(bool disposing) {
_lock.Enter();
try {
if (_disposed) return;
+ _disposed = true;
// Dispose managed state (managed objects)
if (disposing) {
+ // Dispose of the delegate queue wait handle
+ _delegateEnqueuedWaitHandle.Set();
+
// Dispose of the underlying wait handles
EnqueuedDelegate[] items = _queue.Items;
for (int i = 0; i < items.Length; i++) {
@@ -280,13 +310,11 @@ private void Dispose(bool disposing) {
// Dispose the queue full wait handle
_queueFullWaitHandle.Dispose();
+ _delegateEnqueuedWaitHandle.Dispose();
}
// Dispose unmanaged state (unmanaged objects)
// ...
-
- // Indicate disposal completion
- _disposed = true;
} finally {
_lock.Exit();
}
@@ -325,7 +353,7 @@ public record struct EnqueuedDelegate {
/// Gets or sets an exception that occurred during execution of the delegate, if any.
///
/// The exception that occurred during execution, or if no exception occurred.
- public required Exception? Exception { get; set; }
+ public required ExceptionDispatchInfo? Exception { get; set; }
///
/// Gets or sets a wait handle that can be used to wait for the delegate to complete execution.
@@ -367,13 +395,9 @@ public void Execute() {
throw new NotSupportedException($"The delegate type {Delegate.GetType()} is not supported by the {nameof(EnqueuedDelegate)} structure.");
}
} catch (Exception e) {
- // If we have an awaiter, pass the exception to it
- // Otherwise, it's an unhandled exception
- if (HasAwaiter) {
- Exception = e;
- } else {
- throw;
- }
+ // Capture the exception dispatch info, then throw if it's unhandled
+ Exception = ExceptionDispatchInfo.Capture(e);
+ if (!HasAwaiter) throw;
}
}
diff --git a/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
index 22925bc..8b6de97 100644
--- a/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
+++ b/CatalystUI/Core/CatalystUI.Threading/ThreadDelegateDispatcher.cs
@@ -13,6 +13,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.ExceptionServices;
using System.Threading;
namespace Catalyst.Threading {
@@ -32,6 +33,11 @@ public sealed class ThreadDelegateDispatcher : IDisposable {
///
public delegate void DispatcherQueueEventHandler(ThreadDelegateDispatcher dispatcher, Delegate @delegate);
+ ///
+ /// An event handler delegate for exception events related to the .
+ ///
+ public delegate void DispatcherQueueExceptionEventHandler(ThreadDelegateDispatcher dispatcher, Delegate @delegate, ExceptionDispatchInfo? exception);
+
///
/// Invoked prior to the execution of the delegate queue.
///
@@ -57,6 +63,12 @@ public sealed class ThreadDelegateDispatcher : IDisposable {
///
public event DispatcherQueueEventHandler? DelegateExecuted;
+ ///
+ public event DispatcherQueueExceptionEventHandler? DelegateException;
+
+ ///
+ public event DispatcherQueueEventHandler? DelegateFinished;
+
///
/// The default sizing for the delegate caches when using the typed execute methods.
///
@@ -141,6 +153,8 @@ private ThreadDelegateDispatcher(Thread thread, int threadId, int? queueSize = n
_queue.DelegateEnqueued += HandleDelegateEnqueued;
_queue.DelegateDequeued += HandleDelegateDequeued;
_queue.DelegateExecuted += HandleDelegateExecuted;
+ _queue.DelegateException += HandleDelegateException;
+ _queue.DelegateFinished += HandleDelegateFinished;
_disposed = false;
_lock = new();
@@ -225,8 +239,16 @@ public static void Capture(Action callback, int? queue
);
if (isMainThread) MainThreadDispatcher = dispatcher;
// manually enqueue to prevent our smart thread id checking from just calling it directly
- dispatcher._queue.Enqueue(() => callback(dispatcher), nint.Zero, nint.Zero, out _);
+ ExceptionDispatchInfo? exception = null;
+ dispatcher._queue.Enqueue(() => {
+ try {
+ callback(dispatcher);
+ } catch (Exception e) {
+ exception = ExceptionDispatchInfo.Capture(e);
+ }
+ }, nint.Zero, nint.Zero, out _);
dispatcher.WorkerThread();
+ exception?.Throw();
}
///
@@ -537,6 +559,16 @@ private void HandleDelegateExecuted(DelegateQueue queue, DelegateQueue.EnqueuedD
OnDelegateExecuted(@delegate.Delegate);
}
+ ///
+ private void HandleDelegateException(DelegateQueue queue, DelegateQueue.EnqueuedDelegate @delegate, ExceptionDispatchInfo? exception) {
+ OnDelegateException(@delegate.Delegate, exception);
+ }
+
+ ///
+ private void HandleDelegateFinished(DelegateQueue queue, DelegateQueue.EnqueuedDelegate @delegate) {
+ OnDelegateFinished(@delegate.Delegate);
+ }
+
///
private void OnPreExecute() {
PreExecute?.Invoke(this);
@@ -562,22 +594,27 @@ private void OnDelegateExecuted(Delegate @delegate) {
DelegateExecuted?.Invoke(this, @delegate);
}
+ ///
+ private void OnDelegateException(Delegate @delegate, ExceptionDispatchInfo? exception) {
+ DelegateException?.Invoke(this, @delegate, exception);
+ }
+
+ ///
+ private void OnDelegateFinished(Delegate @delegate) {
+ DelegateFinished?.Invoke(this, @delegate);
+ }
+
///
/// The worker thread method that processes the delegate queue.
///
private void WorkerThread() {
while (true) {
- _lock.Enter();
- try {
- if (_disposed) break;
- OnPreExecute();
- if (_disposed) break;
- _queue.Execute();
- if (_disposed) break;
- OnPostExecute();
- } finally {
- _lock.Exit();
- }
+ if (_disposed) break;
+ OnPreExecute();
+ if (_disposed) break;
+ _queue.Execute();
+ if (_disposed) break;
+ OnPostExecute();
}
}
@@ -596,20 +633,20 @@ private void Dispose(bool disposing) {
_lock.Enter();
try {
if (_disposed) return;
+ _disposed = true;
// Dispose managed state (managed objects)
if (disposing) {
_queue.DelegateEnqueued -= HandleDelegateEnqueued;
_queue.DelegateDequeued -= HandleDelegateDequeued;
_queue.DelegateExecuted -= HandleDelegateExecuted;
+ _queue.DelegateException -= HandleDelegateException;
+ _queue.DelegateFinished -= HandleDelegateFinished;
_queue.Dispose();
}
// Dispose unmanaged state (unmanaged objects)
// ...
-
- // Indicate disposal completion
- _disposed = true;
} finally {
_lock.Exit();
}
diff --git a/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/CatalystUI.Examples.BasicWindow.csproj b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/CatalystUI.Examples.BasicWindow.csproj
new file mode 100644
index 0000000..c2ea755
--- /dev/null
+++ b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/CatalystUI.Examples.BasicWindow.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Exe
+ Catalyst.Examples.BasicWindow
+ Catalyst.Examples.BasicWindow
+
+
+
+
+ BasicWindow
+
+
+ BasicWindow.Debug
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Program.cs b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Program.cs
new file mode 100644
index 0000000..1f67220
--- /dev/null
+++ b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Program.cs
@@ -0,0 +1,106 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Builders;
+using Catalyst.Builders.Extensions;
+using Catalyst.Debugging;
+using Catalyst.Modules.Crystal;
+using Catalyst.Supplementary;
+using Catalyst.Threading;
+
+namespace Catalyst.Examples.BasicWindow {
+
+ ///
+ /// Program entry point for the Basic Window example application.
+ ///
+ public static class Program {
+
+ ///
+ /// The debug context for the application.
+ ///
+ private static DebugContext _debug;
+
+ ///
+ /// Static constructor for .
+ ///
+ static Program() {
+ _debug = null!;
+ }
+
+ ///
+ /// Main entry point for the Basic Window example application.
+ ///
+ /// The command-line arguments.
+ public static void Main(string[] args) {
+ new CatalystAppBuilder()
+#if DEBUG
+ .UseCatalystDebug()
+#endif
+ .AddCrystalGlfw3Module()
+ .AddCrystalGlfw3OpenGLSurfaceConnector()
+ .AddCrystalOpenGLModule()
+ .Build(Run);
+ }
+
+ ///
+ /// The main execution method for the application.
+ ///
+ /// The Catalyst application instance.
+ public static void Run(CatalystApp app) {
+ // Debug context initialization
+ _debug = CatalystDebug.ForContext("BasicWindow");
+ _debug.LogInfo("Hello, world!");
+
+ // Layer requests (Since we only include one set of modules, Catalyst will automatically provide the correct implementations)
+ IWindowLayer windowLayer = ModelRegistry.RequestLayer();
+ ISurfaceConnector surfaceConnector = ModelRegistry.RequestConnector();
+ IRendererLayer rendererLayer = ModelRegistry.RequestLayer();
+
+ // Construct the window
+ WindowOptions options = new();
+ if (!SystemDetector.IsSystem()) {
+ // MacOS requires windowing to run on the main thread,
+ // but other systems allow a dedicated window thread.
+ options.Dispatcher = ThreadDelegateDispatcher.New("WindowThread");
+ }
+ options.Hidden = true; // hide until first frame
+ using IWindow window = windowLayer.CreateWindow(options);
+
+ // Construct the render target
+ IRenderTarget target = surfaceConnector.CreateRenderTarget(window);
+
+ // Construct the renderer
+ using IRenderer renderer = rendererLayer.CreateRenderer(new() {
+ Dispatcher = ThreadDelegateDispatcher.New("RenderThread"), // all platforms support a dedicated render thread
+ Target = target,
+
+ // Optional: Enable V-Sync
+ //UseVSync = true,
+
+ // Optional: Enable frame-rate measuring
+ //TargetFrameRate = ushort.MaxValue,
+ //ShouldMeasureFrameRate = true,
+
+ FirstFrameHandler = _ => {
+ // Show the window once the first frame is ready
+ // ReSharper disable AccessToDisposedClosure
+ if (window is { IsClosed: false, IsHidden: true }) window.Show();
+ // ReSharper disable AccessToDisposedClosure
+ }
+ });
+
+ // Wait for the window to close
+ window.Wait();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Properties/PublishProfiles/PublishExamples.pubxml b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Properties/PublishProfiles/PublishExamples.pubxml
new file mode 100644
index 0000000..e08cdfe
--- /dev/null
+++ b/CatalystUI/Examples/CatalystUI.Examples.BasicWindow/Properties/PublishProfiles/PublishExamples.pubxml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ Exe
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+ ConsoleApp
+ bin\Release\net9.0\_publish\
+
+
+ <_Arch>$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString())
+ win-arm64
+ win-x64
+ osx-arm64
+ osx-x64
+ linux-arm64
+ linux-x64
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs
index 5c37d90..8df8524 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/IRendererLayer.cs
@@ -19,7 +19,17 @@ namespace Catalyst.Modules.Crystal {
///
public interface IRendererLayer : IRendererLayer {
- // ...
+ ///
+ /// Constructs a new renderer with the specified options.
+ ///
+ ///
+ /// It is generally recommended to provide a custom
+ /// in the instead of using the main thread to avoid
+ /// blocking windowing operations.
+ ///
+ /// The options to use when creating the renderer, or to use defaults.
+ /// The created renderer.
+ IRenderer CreateRenderer(RendererOptions? options = null);
}
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/ISurfaceConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/ISurfaceConnector.cs
new file mode 100644
index 0000000..d8254b4
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/ISurfaceConnector.cs
@@ -0,0 +1,30 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Connectors;
+
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents the surface connector for visual or graphical windowing to rendering.
+ ///
+ public interface ISurfaceConnector : ISurfaceConnector {
+
+ ///
+ /// Creates a render target for the specified window.
+ ///
+ /// The window to create the render target for.
+ /// The created render target.
+ IRenderTarget CreateRenderTarget(IWindow window);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/NativeConnectors/EdidHelper.cs
similarity index 96%
rename from CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs
rename to CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/NativeConnectors/EdidHelper.cs
index c063ec1..48a9f0b 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/EdidHelper.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/NativeConnectors/EdidHelper.cs
@@ -12,7 +12,7 @@
using System.Text;
// ReSharper disable once CheckNamespace
-namespace Catalyst.Modules.Crystal.Glfw3 {
+namespace Catalyst.Modules.Crystal {
// TODO: Maybe make this an Arcane thing? Seems reasonable enough.
///
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs
index e0edf99..01e5885 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderLayer.cs
@@ -9,6 +9,8 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
+
+
// ReSharper disable once CheckNamespace
namespace Catalyst.Modules.Crystal {
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs
index 6cd6f0d..b0e64d9 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderTarget.cs
@@ -9,6 +9,8 @@
// For full terms, see the LICENSE and NOTICE files in the project root.
// -------------------------------------------------------------------------------------------------
+
+
// ReSharper disable once CheckNamespace
namespace Catalyst.Modules.Crystal {
@@ -115,16 +117,24 @@ public interface IRenderTarget {
double SurfaceHeight { get; }
///
- /// Gets the pixel density of the surface.
+ /// Gets the PPI (pixels per inch) of the surface.
+ ///
+ /// The PPI of the surface.
+ ///
+ double SurfacePpi { get; }
+
+ ///
+ /// Gets the scaling factor of the surface.
///
- /// The pixel density of the surface.
- double SurfacePixelDensity { get; }
+ /// The scaling factor of the surface.
+ ///
+ double SurfaceScalingFactor { get; }
///
- /// Gets the DPI (dots per inch) of the surface.
+ /// Gets a value indicating whether the render target is currently visible.
///
- /// The DPI of the surface.
- double SurfaceDpi { get; }
+ /// if the render target is visible; otherwise, .
+ bool IsVisible { get; }
///
/// Gets a value indicating whether the render target is currently enabled.
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs
index 5ad0e6b..7640d4f 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/IRenderer.cs
@@ -10,6 +10,7 @@
// -------------------------------------------------------------------------------------------------
using Catalyst.Mathematics.Color;
+using Catalyst.Threading;
using System;
using System.Collections.Generic;
@@ -55,6 +56,12 @@ public interface IRenderer : IDisposable {
///
event RendererEventHandler? FrameEnd;
+ ///
+ /// Gets the threading dispatcher associated with the renderer.
+ ///
+ /// The renderer's thread dispatcher.
+ ThreadDelegateDispatcher Dispatcher { get; }
+
///
/// Gets or sets the render target for the renderer.
///
@@ -137,8 +144,8 @@ public interface IRenderer : IDisposable {
///
/// Sets the render target for the renderer.
///
- /// The render target to set.
- void SetTarget(IRenderTarget target);
+ /// The render target to set, or to clear the target.
+ void SetTarget(IRenderTarget? target);
///
/// Attempts to register a new render layer with the renderer.
@@ -163,7 +170,8 @@ public interface IRenderer : IDisposable {
/// Clears the rendering surface with the specified color.
///
/// The color to clear the surface with.
- void Clear(Color128 color);
+ /// to also clear the depth buffer if available; otherwise, .
+ void Clear(Color128 color, bool depth = false);
}
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RenderLayerException.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RenderLayerException.cs
new file mode 100644
index 0000000..33c7502
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RenderLayerException.cs
@@ -0,0 +1,49 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents an error that occurs during a render layer's rendering operations.
+ ///
+ ///
+ public class RenderLayerException : Exception {
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RenderLayerException() : base() {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ public RenderLayerException(string message) : base(message) {
+ // ...
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception
+ /// that is the cause of this exception.
+ ///
+ public RenderLayerException(string message, Exception innerException) : base(message, innerException) {
+ // ...
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererOptions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererOptions.cs
new file mode 100644
index 0000000..7bc5e2b
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Renderer/RendererOptions.cs
@@ -0,0 +1,103 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Threading;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal {
+
+ ///
+ /// Represents options for constructing/configuring a renderer.
+ ///
+ public record struct RendererOptions : IOptions {
+
+ /// The renderer's thread dispatcher, or to use a captured .
+ ///
+ public ThreadDelegateDispatcher? Dispatcher { get; set; } = null;
+
+ ///
+ public IRenderTarget? Target { get; set; } = null;
+
+ ///
+ public IRenderLayer[] Layers { get; set; } = [];
+
+ ///
+ public bool UseVSync { get; set; } = false;
+
+ ///
+ public ushort TargetFrameRate { get; set; } = 0;
+
+ ///
+ public bool ShouldMeasureFrameRate { get; set; } = false;
+
+ ///
+ /// Invoked after the renderer is initialized and
+ /// prepared to be created, but prior to the
+ /// first frame being rendered.
+ ///
+ public IRenderer.RendererEventHandler? InitializedHandler { get; set; } = null;
+
+ ///
+ /// Invoked after the first frame is rendered for
+ /// each assigned target.
+ ///
+ ///
+ /// The event is only invoked once per target,
+ /// after which it is never invoked again
+ /// unless another target is assigned, in
+ /// which case it will be invoked once
+ /// for that target after its first frame
+ /// is rendered.
+ ///
+ public IRenderer.RendererEventHandler? FirstFrameHandler { get; set; } = null;
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The renderer's thread dispatcher.
+ /// The renderer's target.
+ /// The renderer's layers.
+ /// to use VSync; otherwise, .
+ /// The renderer's target frame rate.
+ /// to measure the frame rate; otherwise, .
+ /// An optional handler invoked when the renderer is initialized.
+ /// An optional handler invoked after the first frame is rendered.
+ public RendererOptions(
+ ThreadDelegateDispatcher? dispatcher = null,
+ IRenderTarget? target = null,
+ IRenderLayer[]? layers = null,
+ bool useVSync = false,
+ ushort targetFrameRate = 0,
+ bool shouldMeasureFrameRate = false,
+ IRenderer.RendererEventHandler? initializedHandler = null,
+ IRenderer.RendererEventHandler? firstFrameHandler = null
+ ) {
+ Dispatcher = dispatcher;
+ Target = target;
+ Layers = layers ?? [];
+ UseVSync = useVSync;
+ TargetFrameRate = targetFrameRate;
+ ShouldMeasureFrameRate = shouldMeasureFrameRate;
+ InitializedHandler = initializedHandler;
+ FirstFrameHandler = firstFrameHandler;
+ }
+
+ ///
+ /// Constructs a new
+ /// with default values.
+ ///
+ public RendererOptions() {
+ // ...
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs
index 28f0cc6..37cdc69 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/IWindow.cs
@@ -159,6 +159,15 @@ public interface IWindow : IDisposable {
///
event WindowClosingEventHandler? Closing;
+ ///
+ /// Invoked when the window is disposing of its resources.
+ ///
+ ///
+ /// Raised after the window close operation has been
+ /// confirmed, but before resources have been released.
+ ///
+ event WindowEventHandler? Disposing;
+
///
/// Invoked when the window has closed and resources have been released.
///
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs
index f0634dc..842e4d8 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Core/Window/WindowOptions.cs
@@ -17,45 +17,45 @@ namespace Catalyst.Modules.Crystal {
///
/// Represents options for constructing/configuring a window.
///
- public readonly record struct WindowOptions : IOptions {
+ public record struct WindowOptions : IOptions {
/// The window's thread dispatcher, or to use a captured .
///
- public ThreadDelegateDispatcher? Dispatcher { get; init; } = null;
+ public ThreadDelegateDispatcher? Dispatcher { get; set; } = null;
///
- public uint Width { get; init; } = IWindow.DEFAULT_WIDTH;
+ public uint Width { get; set; } = IWindow.DEFAULT_WIDTH;
///
- public uint Height { get; init; } = IWindow.DEFAULT_HEIGHT;
+ public uint Height { get; set; } = IWindow.DEFAULT_HEIGHT;
///
- public string Title { get; init; } = IWindow.DEFAULT_TITLE;
+ public string Title { get; set; } = IWindow.DEFAULT_TITLE;
///
- public bool Hidden { get; init; } = false;
+ public bool Hidden { get; set; } = false;
///
- public bool Resizable { get; init; } = true;
+ public bool Resizable { get; set; } = true;
///
- public bool Decorated { get; init; } = true;
+ public bool Decorated { get; set; } = true;
///
- public WindowFullscreenMode FullscreenMode { get; init; } = WindowFullscreenMode.Windowed;
+ public WindowFullscreenMode FullscreenMode { get; set; } = WindowFullscreenMode.Windowed;
///
- public ushort PollRate { get; init; } = 0;
+ public ushort PollRate { get; set; } = 0;
///
- public WindowIcon[]? Icons { get; init; } = null;
+ public WindowIcon[]? Icons { get; set; } = null;
///
/// Invoked after the window is initialized and
/// prepared to be created, but prior to the
/// actual construction of the window.
///
- public IWindow.WindowEventHandler? InitializedHandler { get; init; } = null;
+ public IWindow.WindowEventHandler? InitializedHandler { get; set; } = null;
///
/// Constructs a new .
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs
index 33a1a74..5cef707 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Extensions/CatalystAppBuilderExtensions.cs
@@ -40,11 +40,11 @@ public static CatalystAppBuilder AddCrystalGlfw3Module(this CatalystAppBuilder b
Glfw3WindowLayer glfw3WindowingLayer = new();
ModelRegistry.RegisterLayer(glfw3WindowingLayer);
if (SystemDetector.IsSystem()) {
- Glfw3WindowsNativeHandler windowsHandler = new();
- ModelRegistry.RegisterConnector(windowsHandler);
+ Glfw3WindowsNativeConnector windowsConnector = new();
+ ModelRegistry.RegisterConnector(windowsConnector);
} else if (SystemDetector.IsSystem()) {
- Glfw3MacNativeHandler macHandler = new();
- ModelRegistry.RegisterConnector(macHandler);
+ Glfw3MacNativeConnector macConnector = new();
+ ModelRegistry.RegisterConnector(macConnector);
} else if (SystemDetector.IsSystem()) {
throw new NotImplementedException();
} else {
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs
index 580adb0..a381f02 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Glfw3.cs
@@ -108,7 +108,7 @@ private Glfw3() {
public static Glfw3 GetInstance(ThreadDelegateDispatcher? dispatcher = null) {
_staticLock.Enter();
try {
- if (_referenceCount == 0) Initialize();
+ if (_referenceCount == 0) Initialize(dispatcher);
_referenceCount++;
return new();
} finally {
@@ -126,7 +126,7 @@ private static void Initialize(ThreadDelegateDispatcher? dispatcher = null) {
dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
}
if (!dispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) {
- throw new TypeInitializationException(nameof(Glfw3), new InvalidOperationException("Failed to initialize Glfw3 API on the main thread."));
+ throw new TypeInitializationException(nameof(Glfw3), new InvalidOperationException("Failed to initialize Glfw3 API."));
}
_initDispatcher = dispatcher;
}
@@ -147,7 +147,7 @@ private static void Terminate(ThreadDelegateDispatcher? dispatcher = null) {
dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
}
if (!dispatcher.Execute(_cachedActionTerminateUnsafe, wait: true)) {
- throw new InvalidOperationException("Failed to terminate Glfw3 API on the main thread.");
+ throw new InvalidOperationException("Failed to terminate Glfw3 API.");
}
}
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs
index 1e430f1..6b2af4d 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3MacNativeConnector.cs
@@ -22,7 +22,7 @@ namespace Catalyst.Modules.Crystal.Glfw3 {
///
/// An Apple Mac-based implementation of
///
- public sealed unsafe class Glfw3MacNativeHandler : IGlfw3NativeConnector {
+ public sealed unsafe class Glfw3MacNativeConnector : IGlfw3NativeConnector {
///
public nint GetNativeHandle(Glfw3 glfw, WindowHandle* pWindow) {
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs
index fdfb9fe..a7d53a2 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/NativeConnectors/Glfw3WindowsNativeConnector.cs
@@ -23,7 +23,7 @@ namespace Catalyst.Modules.Crystal.Glfw3 {
///
/// A Microsoft Windows-based implementation of .
///
- public sealed unsafe class Glfw3WindowsNativeHandler : IGlfw3NativeConnector {
+ public sealed unsafe class Glfw3WindowsNativeConnector : IGlfw3NativeConnector {
///
public nint GetNativeHandle(Glfw3 glfw, WindowHandle* pWindow) {
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs
index 5aad99d..039149c 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwDisplay.cs
@@ -25,7 +25,7 @@ namespace Catalyst.Modules.Crystal.Glfw3 {
public readonly record struct GlfwDisplay : IDisplay {
///
- /// Gets the Glfw3 monitor handle for this display.
+ /// Gets the Glfw3 monitor handle for the display.
///
/// The display's Glfw3 monitor handle.
public required nint Monitor { get; init; }
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs
index 1fce8e0..bdbcecb 100644
--- a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3/Window/GlfwWindow.cs
@@ -35,7 +35,7 @@ public unsafe class GlfwWindow : IWindow {
/// The minimum polling rate for the window in milliseconds.
///
///
- /// Prevents lockups of the main thread when the
+ /// Prevents lockups of the thread when the
/// window may not be responding.
///
public const ushort MINIMUM_POLL_RATE = 3000;
@@ -82,6 +82,9 @@ public unsafe class GlfwWindow : IWindow {
///
public event IWindow.WindowClosingEventHandler? Closing;
+ ///
+ public event IWindow.WindowEventHandler? Disposing;
+
///
public event IWindow.WindowEventHandler? Closed;
@@ -478,6 +481,7 @@ public GlfwWindow(WindowOptions options) {
Glfw3.DebugContext.Log(LogLevel.Verbose, "Done.");
// Assign cached displays to avoid multiple creations/destruction of the API
+ _cachedPrimaryDisplay = Glfw3WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(Dispatcher);
_cachedDisplays = Glfw3WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher);
// Specify no client API
@@ -1024,7 +1028,7 @@ protected virtual void OnErrored(WindowException exception) {
///
protected virtual void OnCreated() {
- Glfw3.DebugContext.Log(LogLevel.Info, $"Window created.", prefix: LogId);
+ Glfw3.DebugContext.Log(LogLevel.Info, "Window created.", prefix: LogId);
Created?.Invoke(this);
}
@@ -1100,6 +1104,12 @@ protected virtual bool OnClosing() {
if (result) Glfw3.DebugContext.Log(LogLevel.Debug, "Window closure cancelled by event handler.", prefix: LogId);
return result;
}
+
+ ///
+ protected virtual void OnDisposing() {
+ Glfw3.DebugContext.Log(LogLevel.Info, "Window disposing...", prefix: LogId);
+ Disposing?.Invoke(this);
+ }
///
protected virtual void OnClosed() {
@@ -1128,12 +1138,15 @@ private void Dispose(bool disposing) {
try {
if (_disposed) return;
+ // Disposing
+ OnDisposing();
+
// Dispose managed state (managed objects)
if (disposing) {
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (Dispatcher != null) {
- Dispatcher.PreExecute -= HandlePreExecute;
- Dispatcher.DelegateEnqueued -= HandleDelegateEnqueued;
+ Dispatcher.PreExecute -= _handlePreExecute;
+ Dispatcher.DelegateEnqueued -= _handleDelegateEnqueued;
}
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector.csproj b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector.csproj
new file mode 100644
index 0000000..d5ceb40
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Catalyst.Modules.Crystal.Connectors
+ Catalyst.Modules.Crystal.Glfw3OpenGLSurfaceConnector
+ true
+
+
+ CatalystUI Crystal – Glfw3OpenGLSurfaceConnector
+ 1.0.0
+ alpha.1
+ CatalystUI LLC
+ Glfw3 to OpenGL API for the Crystal subset of modules provided by the CatalystUI library.
+ CatalystUI,Crystal,glfw3,glfw,opengl,gl,ui,gui,graphics,visual,window
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Extensions/CatalystAppBuilderExtensions.cs
new file mode 100644
index 0000000..2936246
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Extensions/CatalystAppBuilderExtensions.cs
@@ -0,0 +1,45 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+// ReSharper disable once CheckNamespace
+
+using Catalyst.Modules.Crystal.Connectors;
+
+namespace Catalyst.Builders.Extensions {
+
+ ///
+ /// Builder extensions for the .
+ ///
+ public static class CatalystAppBuilderExtensions {
+
+ ///
+ /// Adds the Crystal-based Glfw3 to OpenGL surface connector to the .
+ ///
+ ///
+ ///
+ /// The Crystal-based Glfw3 to OpenGL surface connector adds the following to your CatalystUI application:
+ ///
+ ///
+ ///
+ /// Click on any of the above links to learn more about each component.
+ ///
+ ///
+ /// The to add the connector to.
+ /// The with the Glfw3 to OpenGL surface connector added.
+ public static CatalystAppBuilder AddCrystalGlfw3OpenGLSurfaceConnector(this CatalystAppBuilder builder) {
+ Glfw3OpenGLSurfaceConnector connector = new();
+ ModelRegistry.RegisterConnector(connector);
+ return builder;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLRenderTarget.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLRenderTarget.cs
new file mode 100644
index 0000000..91f2c34
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLRenderTarget.cs
@@ -0,0 +1,151 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.Glfw3;
+using Catalyst.Supplementary;
+using System;
+
+namespace Catalyst.Modules.Crystal.Connectors {
+
+ ///
+ /// A Crystal implementation for a render target
+ /// between a Glfw3 window and an OpenGL renderer.
+ ///
+ public class Glfw3OpenGLRenderTarget : IRenderTarget {
+
+ ///
+ public event IRenderTarget.RenderTargetEventHandler? Updated;
+
+ ///
+ public event IRenderTarget.RenderTargetEventHandler? Destroying;
+
+ ///
+ /// The window associated with the render target.
+ ///
+ protected readonly IWindow _window;
+
+ ///
+ /// Internal reference for .
+ ///
+ private nint[]? _nativeHandle;
+
+ ///
+ public nint[] NativeHandle {
+ get {
+ if (_nativeHandle == null) {
+ // cache it for performance
+ if (SystemDetector.IsSystem()) {
+ _nativeHandle = [_window.NativeHandle[0]];
+ } else if (SystemDetector.IsSystem()) {
+ _nativeHandle = [_window.NativeHandle[0]];
+ } else if (SystemDetector.IsSystem()) {
+ throw new NotImplementedException();
+ } else {
+ _nativeHandle = [];
+ }
+ }
+ return _nativeHandle;
+ }
+ }
+
+ ///
+ public double X => _window.X;
+
+ ///
+ public double Y => _window.Y;
+
+ ///
+ public uint Width => _window.Width;
+
+ ///
+ public uint Height => _window.Height;
+
+ ///
+ public double SurfaceX => _window.Display?.X ?? 0;
+
+ ///
+ public double SurfaceY => _window.Display?.Y ?? 0;
+
+ ///
+ public double SurfaceWidth => _window.Display?.Width ?? 0;
+
+ ///
+ public double SurfaceHeight => _window.Display?.Height ?? 0;
+
+ ///
+ public double SurfacePpi => _window.Display?.PixelsPerInch ?? 96.0;
+
+ ///
+ public double SurfaceScalingFactor => _window.Display?.ScalingFactor ?? 1.0;
+
+ ///
+ public bool IsVisible => _window.IsVisible;
+
+ ///
+ public bool IsEnabled { get; set; }
+
+ // Cached delegates for callbacks for performance
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ protected readonly IWindow.WindowEventHandler _handleWindowUpdated;
+ protected readonly IWindow.WindowEventHandler _handleWindowDisposing;
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The associated with the render target.
+ public Glfw3OpenGLRenderTarget(GlfwWindow window) {
+ // Fields
+ _window = window;
+ _handleWindowUpdated = HandleWindowUpdated;
+ _handleWindowDisposing = HandleWindowDisposing;
+
+ // Properties
+ IsEnabled = true;
+
+ // Attach events
+ _window.Refresh += _handleWindowUpdated;
+ _window.Redraw += _handleWindowUpdated;
+ _window.Resized += _handleWindowUpdated;
+ _window.Repositioned += _handleWindowUpdated;
+ _window.Maximized += _handleWindowUpdated;
+ _window.Minimized += _handleWindowUpdated;
+ _window.Focused += _handleWindowUpdated;
+ _window.Shown += _handleWindowUpdated;
+ _window.Disposing += _handleWindowDisposing;
+ }
+
+ ///
+ /// Used as a catch-all for window updates that require a render target update.
+ ///
+ /// The window that was updated.
+ protected virtual void HandleWindowUpdated(IWindow window) {
+ OnUpdated();
+ }
+
+ ///
+ protected virtual void HandleWindowDisposing(IWindow window) {
+ OnDestroying();
+ }
+
+ ///
+ protected virtual void OnUpdated() {
+ Updated?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnDestroying() {
+ Destroying?.Invoke(this);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLSurfaceConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLSurfaceConnector.cs
new file mode 100644
index 0000000..c56dc22
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Glfw3OpenGLSurfaceConnector/Glfw3OpenGLSurfaceConnector.cs
@@ -0,0 +1,31 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.Glfw3;
+using System;
+
+namespace Catalyst.Modules.Crystal.Connectors {
+
+ ///
+ /// The Crystal implementation for a surface connector between
+ /// a Glfw3 window and an OpenGL renderer.
+ ///
+ public sealed class Glfw3OpenGLSurfaceConnector : ISurfaceConnector {
+
+ ///
+ public IRenderTarget CreateRenderTarget(IWindow window) {
+ if (window is not GlfwWindow glfwWindow) throw new ArgumentException("The provided window is not a GlfwWindow.", nameof(window));
+ return new Glfw3OpenGLRenderTarget(glfwWindow);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/CatalystUI.Modules.Crystal.OpenGL.csproj b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/CatalystUI.Modules.Crystal.OpenGL.csproj
new file mode 100644
index 0000000..fac9425
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/CatalystUI.Modules.Crystal.OpenGL.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Catalyst.Modules.Crystal.OpenGL
+ Catalyst.Modules.Crystal.OpenGL
+ true
+
+
+ CatalystUI Crystal – OpenGL
+ 1.0.0
+ alpha.1
+ CatalystUI LLC
+ OpenGL API for the Crystal subset of modules provided by the CatalystUI library.
+ CatalystUI,Crystal,opengl,gl,ui,gui,graphics,visual,window
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Extensions/CatalystAppBuilderExtensions.cs
new file mode 100644
index 0000000..067fe91
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Extensions/CatalystAppBuilderExtensions.cs
@@ -0,0 +1,57 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.OpenGL;
+using Catalyst.Supplementary;
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Builders.Extensions {
+
+ ///
+ /// Builder extensions for the .
+ ///
+ public static class CatalystAppBuilderExtensions {
+
+ ///
+ /// Adds the Crystal-based OpenGL rendering module to the .
+ ///
+ ///
+ ///
+ /// The Crystal-based OpenGL rendering module adds the following to your CatalystUI application:
+ ///
+ ///
+ ///
+ ///
+ /// Click on any of the above links to learn more about each component.
+ ///
+ ///
+ /// The to add the module to.
+ /// The with the OpenGL rendering module added.
+ public static CatalystAppBuilder AddCrystalOpenGLModule(this CatalystAppBuilder builder) {
+ OpenGLRendererLayer openGLRendererLayer = new();
+ ModelRegistry.RegisterLayer(openGLRendererLayer);
+ if (SystemDetector.IsSystem()) {
+ throw new NotImplementedException();
+ } else if (SystemDetector.IsSystem()) {
+ OpenGLMacNativeConnector macConnector = new();
+ ModelRegistry.RegisterConnector(macConnector);
+ } else if (SystemDetector.IsSystem()) {
+ throw new NotImplementedException();
+ } else {
+ throw new PlatformNotSupportedException("The Crystal OpenGL module is not supported on this platform. Consider using a different rendering module or contributing a native connector for this platform.");
+ }
+ return builder;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/IOpenGLNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/IOpenGLNativeConnector.cs
new file mode 100644
index 0000000..7514ce3
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/IOpenGLNativeConnector.cs
@@ -0,0 +1,72 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Connectors;
+using Catalyst.Domains;
+using Catalyst.Layers;
+
+namespace Catalyst.Modules.Crystal.OpenGL {
+
+ ///
+ /// The Crystal interface of a native connector for the OpenGL rendering API.
+ ///
+ ///
+ public interface IOpenGLNativeConnector : INativeConnector where TLayerLow : ISystemLayer {
+
+ ///
+ /// Gets the address of an OpenGL function by name.
+ ///
+ /// The name of the OpenGL function.
+ /// The address of the OpenGL function.
+ nint GetProcAddress(string name);
+
+ ///
+ /// Creates an OpenGL context for the specified render target.
+ ///
+ /// The OpenGL instance.
+ /// The render target.
+ /// The handle of the created OpenGL context.
+ nint CreateContext(OpenGL gl, IRenderTarget target);
+
+ ///
+ /// Attempts to delete the specified OpenGL context for the given render target.
+ ///
+ /// The handle of the OpenGL context to delete.
+ /// The render target.
+ /// if the context was successfully deleted; otherwise, .
+ bool TryDeleteContext(nint context, IRenderTarget target);
+
+ ///
+ /// Attempts to make the specified OpenGL context current for the given render target.
+ ///
+ /// The handle of the OpenGL context to make current.
+ /// The render target.
+ /// if the context was successfully made current; otherwise, .
+ bool TryMakeContextCurrent(nint context, IRenderTarget target);
+
+ ///
+ /// Swaps the front and back buffers of the specified
+ /// render target using the current OpenGL context.
+ ///
+ /// The render target.
+ /// if the buffers were successfully swapped; otherwise, .
+ bool TrySwapBuffers(IRenderTarget target);
+
+ ///
+ /// Sets the swap interval for the current OpenGL context.
+ ///
+ /// 0 to disable V-Sync, 1 to enable V-Sync.
+ /// if the swap interval was successfully set; otherwise, .
+ bool TrySetSwapInterval(int interval);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/NativeConnectors/OpenGLMacNativeConnector.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/NativeConnectors/OpenGLMacNativeConnector.cs
new file mode 100644
index 0000000..9832b74
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/NativeConnectors/OpenGLMacNativeConnector.cs
@@ -0,0 +1,162 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Supplementary;
+using Catalyst.Threading;
+using System;
+using System.Runtime.InteropServices;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.OpenGL {
+
+ ///
+ /// An Apple Mac-based implementation of .
+ ///
+ public unsafe class OpenGLMacNativeConnector : IOpenGLNativeConnector {
+
+ ///
+ public nint GetProcAddress(string name) {
+ nint proc = MacOpenGLImports.dlsym(MacOpenGLImports._glHandle, name);
+ if (proc == 0) proc = MacOpenGLImports.dlsym(MacOpenGLImports.RTLD_DEFAULT, name);
+ return proc;
+ }
+
+ ///
+ public nint CreateContext(OpenGL gl, IRenderTarget target) {
+ // MacOS requires us to do this on the Main thread.
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(OpenGLMacNativeConnector), nameof(CreateContext));
+ nint context = 0;
+ nint makeCurrentSel = MacOpenGLImports.sel_registerName("makeCurrentContext");
+ _ = ThreadDelegateDispatcher.MainThreadDispatcher.Execute(() => {
+ // Fetch classes and selectors
+ nint fmtClass = MacOpenGLImports.objc_getClass("NSOpenGLPixelFormat");
+ nint ctxClass = MacOpenGLImports.objc_getClass("NSOpenGLContext");
+ nint allocSel = MacOpenGLImports.sel_registerName("alloc");
+ nint initAttrSel = MacOpenGLImports.sel_registerName("initWithAttributes:");
+ nint initFormatSel = MacOpenGLImports.sel_registerName("initWithFormat:shareContext:");
+
+ // Attribute list for OpenGL 3.2 Core profile with 24-bit color and depth
+ int[] attribs = [
+ 99, // NSOpenGLPFAOpenGLProfile
+ 0x3200, // NSOpenGLProfileVersion3_2Core
+ 5, // NSOpenGLPFADoubleBuffer
+ 8, 24, // NSOpenGLPFAColorSize
+ 12, 24, // NSOpenGLPFADepthSize
+ 73, // NSOpenGLPFAAccelerated
+ 0 // terminator
+ ];
+
+ // Create the pixel format object
+ nint fmtAlloc = MacOpenGLImports.objc_msgSend(fmtClass, allocSel);
+ nint pixelFormat;
+ fixed (int* attribPtr = attribs) {
+ pixelFormat = MacOpenGLImports.objc_msgSend(fmtAlloc, initAttrSel, (nint) attribPtr);
+ }
+ if (pixelFormat == 0) {
+ throw new InvalidOperationException("Failed to create NSOpenGLPixelFormat.");
+ }
+
+ // Create an OpenGL context
+ nint ctxAlloc = MacOpenGLImports.objc_msgSend(ctxClass, allocSel);
+ context = MacOpenGLImports.objc_msgSend(ctxAlloc, initFormatSel, pixelFormat, 0);
+ if (context == 0) {
+ throw new InvalidOperationException("Failed to create NSOpenGLContext.");
+ }
+
+ // Set the context's view to the target's native handle
+ nint contentViewSel = MacOpenGLImports.sel_registerName("contentView");
+ nint view = MacOpenGLImports.objc_msgSend(target.NativeHandle[0], contentViewSel);
+ nint setViewSel = MacOpenGLImports.sel_registerName("setView:");
+ MacOpenGLImports.objc_msgSend(context, setViewSel, view);
+ }, wait: true);
+
+ // Make the context current
+ MacOpenGLImports.objc_msgSend(context, makeCurrentSel);
+
+ // Return the created context
+ return context;
+ }
+
+ ///
+ public bool TryDeleteContext(nint context, IRenderTarget target) {
+ // Context deletion is handled by the Objective-C runtime (neat!)
+ return true;
+ }
+
+ ///
+ public bool TryMakeContextCurrent(nint context, IRenderTarget target) {
+ nint makeCurrentSel = MacOpenGLImports.sel_registerName("makeCurrentContext");
+ MacOpenGLImports.objc_msgSend(context, makeCurrentSel);
+ return true;
+ }
+
+ ///
+ public bool TrySwapBuffers(IRenderTarget target) {
+ nint currentSel = MacOpenGLImports.sel_registerName("currentContext");
+ nint context = MacOpenGLImports.objc_msgSend(MacOpenGLImports.objc_getClass("NSOpenGLContext"), currentSel);
+ nint flushSel = MacOpenGLImports.sel_registerName("flushBuffer");
+ MacOpenGLImports.objc_msgSend(context, flushSel);
+ return true;
+ }
+
+ ///
+ public bool TrySetSwapInterval(int interval) {
+ nint currentSel = MacOpenGLImports.sel_registerName("currentContext");
+ nint context = MacOpenGLImports.objc_msgSend(MacOpenGLImports.objc_getClass("NSOpenGLContext"), currentSel);
+ if (context == 0) return false;
+ nint setSwapSel = MacOpenGLImports.sel_registerName("setValues:forParameter:");
+ int[] args = [ interval ];
+ fixed (int* argPtr = args) {
+ MacOpenGLImports.objc_msgSend(context, setSwapSel, (nint)argPtr, 222); // 222 = NSOpenGLCPSwapInterval
+ }
+ return true;
+ }
+
+ }
+
+ // ReSharper disable InconsistentNaming
+ internal static unsafe class MacOpenGLImports {
+
+ // ObjC imports
+ [DllImport("/usr/lib/libobjc.A.dylib")]
+ internal static extern nint objc_getClass(string name);
+
+ [DllImport("/usr/lib/libobjc.A.dylib")]
+ internal static extern nint sel_registerName(string name);
+
+ [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")]
+ internal static extern nint objc_msgSend(nint receiver, nint selector);
+
+ [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")]
+ internal static extern nint objc_msgSend(nint receiver, nint selector, nint arg1);
+
+ [DllImport("/usr/lib/libobjc.A.dylib", EntryPoint = "objc_msgSend")]
+ internal static extern nint objc_msgSend(nint receiver, nint selector, nint arg1, nint arg2);
+
+ internal const int RTLD_LAZY = 0x1;
+ internal static readonly nint _glHandle = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", RTLD_LAZY);
+
+ // From libSystem
+ [DllImport("libSystem.B.dylib", EntryPoint = "dlsym")]
+ internal static extern nint dlsym(nint handle, string symbol);
+
+ [DllImport("libSystem.B.dylib", EntryPoint = "dlopen")]
+ internal static extern nint dlopen(string path, int mode);
+
+ // Externally defined RTLD_DEFAULT (symbol reference)
+ [DllImport("libSystem.B.dylib", EntryPoint = "dlsym")]
+ internal static extern nint dlsym_default(nint handle, [MarshalAs(UnmanagedType.LPStr)] string symbol);
+
+ internal static readonly nint RTLD_DEFAULT = (nint)(-2);
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGL.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGL.cs
new file mode 100644
index 0000000..ad0ef58
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGL.cs
@@ -0,0 +1,207 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Attributes.Threading;
+using Catalyst.Debugging;
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Catalyst.Native;
+using Catalyst.Threading;
+using Silk.NET.OpenGL;
+using System;
+using System.Threading;
+
+namespace Catalyst.Modules.Crystal.OpenGL {
+
+ ///
+ /// Native API wrapper for the OpenGL library.
+ ///
+ public sealed partial class OpenGL : INativeApi {
+
+ ///
+ /// The underlying wrapped API instance.
+ ///
+ private static GL? _api;
+
+ ///
+ /// The dispatcher which was used to initialize the API.
+ ///
+ private static ThreadDelegateDispatcher? _initDispatcher;
+
+ ///
+ /// The total number of 'requests' or instantiations of the API.
+ ///
+ private static ushort _referenceCount;
+
+ ///
+ /// A static lock used to ensure thread-safe access to the static members.
+ ///
+ private static readonly Lock _staticLock;
+
+ ///
+ /// Gets the OpenGL debug context.
+ ///
+ /// The debug context for OpenGL.
+ public static DebugContext DebugContext { get; }
+
+ ///
+ /// Gets the wrapped API instance.
+ ///
+ /// The wrapped OpenGL API instance.
+ public GL Api {
+ get {
+ _staticLock.Enter();
+ try {
+ return _api!; // Non-nullable because it is initialized in the static constructor
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+ }
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ private readonly Lock _lock;
+
+ ///
+ /// Static constructor for .
+ ///
+ static OpenGL() {
+ // Fields
+ _referenceCount = 0;
+ _staticLock = new();
+
+ // Properties
+ DebugContext = CatalystDebug.ForContext("OpenGL");
+ }
+
+ ///
+ /// Constructs a new .
+ ///
+ public OpenGL() {
+ // Fields
+ _disposed = false;
+ _lock = new();
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~OpenGL() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public static OpenGL GetInstance(ThreadDelegateDispatcher? dispatcher = null) {
+ _staticLock.Enter();
+ try {
+ if (_referenceCount == 0) Initialize(dispatcher);
+ _referenceCount++;
+ return new();
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+
+ ///
+ /// Initializes the OpenGL API.
+ ///
+ /// A thread dispatcher to associate with the API instance, or to use a captured main thread dispatcher.
+ private static void Initialize(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(OpenGL), nameof(GetInstance));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) {
+ throw new TypeInitializationException(nameof(OpenGL), new InvalidOperationException("Failed to initialize OpenGL API."));
+ }
+ _initDispatcher = dispatcher;
+ }
+
+ [CachedDelegate]
+ private static void InitializeUnsafe() {
+ if (_api != null) throw new InvalidOperationException("The OpenGL API is already initialized.");
+ IOpenGLNativeConnector> nativeConnector;
+ try {
+ nativeConnector = ModelRegistry.RequestConnector>>();
+ } catch {
+ throw new PlatformNotSupportedException("No suitable OpenGL native connector is available for the current platform.");
+ }
+ _api = GL.GetApi(nativeConnector.GetProcAddress);
+ }
+
+ ///
+ /// Terminates the OpenGL API.
+ ///
+ /// The thread dispatcher associated with the API instance, or to use the captured main thread dispatcher.
+ private static void Terminate(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(OpenGL), nameof(Terminate));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionTerminateUnsafe, wait: true)) {
+ throw new InvalidOperationException("Failed to terminate OpenGL API.");
+ }
+ }
+
+ [CachedDelegate]
+ private static void TerminateUnsafe() {
+ _api?.Dispose();
+ _api = null;
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.Enter();
+ try {
+ if (_disposed) return;
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // ...
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ _staticLock.Enter();
+ try {
+ _referenceCount--;
+ if (_referenceCount == 0) Terminate(_initDispatcher);
+ } finally {
+ _staticLock.Exit();
+ }
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.Exit();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGLRendererLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGLRendererLayer.cs
new file mode 100644
index 0000000..e0d9bff
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/OpenGLRendererLayer.cs
@@ -0,0 +1,29 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.OpenGL.Renderer;
+
+namespace Catalyst.Modules.Crystal.OpenGL {
+
+ ///
+ /// The Crystal implementation of the CatalystUI type
+ /// using the OpenGL rendering API.
+ ///
+ public sealed class OpenGLRendererLayer : IRendererLayer {
+
+ ///
+ public IRenderer CreateRenderer(RendererOptions? options = null) {
+ return new OpenGLRenderer(options ?? new());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Renderer/OpenGLRenderer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Renderer/OpenGLRenderer.cs
new file mode 100644
index 0000000..00730ec
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.OpenGL/Renderer/OpenGLRenderer.cs
@@ -0,0 +1,748 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Collections;
+using Catalyst.Debugging;
+using Catalyst.Domains;
+using Catalyst.Layers;
+using Catalyst.Mathematics.Color;
+using Catalyst.Threading;
+using Silk.NET.OpenGL;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+namespace Catalyst.Modules.Crystal.OpenGL.Renderer {
+
+ ///
+ /// An implementation of using
+ /// the OpenGL rendering API.
+ ///
+ public class OpenGLRenderer : IRenderer {
+
+ ///
+ /// The minimum rendering rate for the renderer in milliseconds.
+ ///
+ ///
+ /// Prevents lockups of the thread when the
+ /// renderer may not be responding.
+ ///
+ public const ushort MINIMUM_RENDER_RATE = 3000;
+
+ ///
+ /// Precise timing depends on the system timer's precision. In
+ /// worst-case scenarios, Windows is the slowest around
+ /// ~15.6 MS. We only use the system timer to wait
+ /// for periods longer than 20 MS or more, then swap
+ /// to a more precise waiting method below that time period.
+ ///
+ public const int PRECISE_TIMING_BUFFER = 16;
+
+ ///
+ /// The number of samples used to calculate the frame rate and delta time.
+ ///
+ public const int FRAME_RATE_SAMPLE_SIZE = 24;
+
+ ///
+ /// The default clear color for the renderer.
+ ///
+ public static readonly Color128 DEFAULT_CLEAR_COLOR = Color128.WHITE;
+
+ ///
+ public event IRenderer.RendererErroredEventHandler? Errored;
+
+ ///
+ public event IRenderer.RendererEventHandler? Created;
+
+ ///
+ public event IRenderer.RendererEventHandler? FrameStart;
+
+ ///
+ public event IRenderer.RendererEventHandler? FrameEnd;
+
+ ///
+ /// Gets a generated log identifier for the renderer.
+ ///
+ /// The renderer class name and context handle in hexadecimal format.
+ protected string LogId => $"{nameof(OpenGLRenderer)} {(OpenGLContext != 0 ? $"0x{OpenGLContext:X}" : "(No Context)")}";
+
+ ///
+ /// Gets or sets the OpenGL instance associated with the renderer.
+ ///
+ /// The OpenGL instance.
+ public OpenGL OpenGL { get; private set; }
+
+ ///
+ /// Gets or sets the OpenGL context handle associated with the renderer.
+ ///
+ /// The OpenGL context handle.
+ public nint OpenGLContext { get; private set; }
+
+ ///
+ /// Gets or sets the OpenGL native connector for the renderer.
+ ///
+ /// The OpenGL native connector.
+ public IOpenGLNativeConnector> OpenGLNative { get; private set; }
+
+ ///
+ public ThreadDelegateDispatcher Dispatcher { get; init; }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile IRenderTarget? _target;
+
+ ///
+ public virtual IRenderTarget? Target {
+ get => _target;
+ set => SetTarget(value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile List _layers;
+
+ ///
+ public virtual IReadOnlyList Layers => _layers;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _useVSync;
+
+ ///
+ public virtual bool UseVSync {
+ get => _useVSync;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ _useVSync = value;
+ if (OpenGLContext == nint.Zero) return;
+ if (!OpenGLNative.TryMakeContextCurrent(OpenGLContext, _target!)) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Failed to make OpenGL context current; cannot update VSync setting.", prefix: LogId);
+ return;
+ }
+ if (_useVSync) {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "Enabling VSync...", prefix: LogId);
+ if (!OpenGLNative.TrySetSwapInterval(1)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, "Failed to enable VSync.", prefix: LogId);
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "VSync enabled.", prefix: LogId);
+ }
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "Disabling VSync...", prefix: LogId);
+ if (!OpenGLNative.TrySetSwapInterval(0)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, "Failed to disable VSync.", prefix: LogId);
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "VSync disabled.", prefix: LogId);
+ }
+ }
+ }, wait: true);
+ }
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile ushort _targetFrameRate;
+
+ ///
+ public virtual ushort TargetFrameRate {
+ get => _targetFrameRate;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _targetFrameRate = value;
+ _ = Dispatcher.Execute(() => {
+ // Signal the poll event to reset any waits
+ });
+ }
+ }
+
+ ///
+ public virtual bool ShouldMeasureFrameRate { get; set; }
+
+ ///
+ public virtual double AvgFrameRate { get; protected set; }
+
+ ///
+ public virtual double AvgDeltaTime { get; protected set; }
+
+ ///
+ /// Used to ensure viewport is only updated when necessary.
+ ///
+ protected uint _previousTargetWidth;
+
+ ///
+ /// Used to ensure viewport is only updated when necessary.
+ ///
+ protected uint _previousTargetHeight;
+
+ ///
+ /// Used to signal polling resets.
+ ///
+ protected readonly ManualResetEvent _resetPollEventHandle;
+
+ ///
+ /// Used to track the current state of the poll event handle.
+ ///
+ protected bool _resetPollEventHandleState;
+
+ ///
+ /// The stopwatch used for frame rate measurement.
+ ///
+ protected readonly Stopwatch _frameRateStopWatch;
+
+ ///
+ /// The last time the frame rate was reported.
+ ///
+ protected DateTime _frameRateLastReportTime;
+
+ ///
+ /// The samples used for average frame rate calculation.
+ ///
+ protected readonly StaticArrayQueue _frameRateSamples;
+
+ // Cached delegates for callbacks for performance
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ protected bool _firstFrame;
+ protected readonly IRenderer.RendererEventHandler? _firstFrameHandler;
+ protected readonly Action _cachedActionRender;
+ protected readonly ThreadDelegateDispatcher.DispatcherEventHandler _handlePreExecute;
+ protected readonly ThreadDelegateDispatcher.DispatcherQueueEventHandler _handleDelegateEnqueued;
+ protected readonly IRenderTarget.RenderTargetEventHandler _handleTargetUpdated;
+ protected readonly IRenderTarget.RenderTargetEventHandler _handleTargetDestroying;
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ protected readonly ReaderWriterLockSlim _lock;
+
+ ///
+ /// Constructs a new .
+ ///
+ public OpenGLRenderer(RendererOptions options) {
+ // Fields
+ _target = null; // handled later
+ _layers = []; // handled later
+ _useVSync = options.UseVSync;
+ _targetFrameRate = options.TargetFrameRate;
+ _previousTargetWidth = 0;
+ _previousTargetHeight = 0;
+ _resetPollEventHandle = new(false);
+ _resetPollEventHandleState = false;
+ _frameRateStopWatch = new();
+ _frameRateLastReportTime = DateTime.UtcNow;
+ _frameRateSamples = new(FRAME_RATE_SAMPLE_SIZE);
+ _firstFrame = true;
+ _firstFrameHandler = options.FirstFrameHandler;
+ _cachedActionRender = Render;
+ _handlePreExecute = HandlePreExecute;
+ _handleDelegateEnqueued = HandleDelegateEnqueued;
+ _handleTargetUpdated = HandleTargetUpdated;
+ _handleTargetDestroying = HandleTargetDestroying;
+ _disposed = false;
+ _lock = new(LockRecursionPolicy.SupportsRecursion);
+
+ // Properties
+ OpenGL = null!;
+ OpenGLContext = nint.Zero;
+ OpenGLNative = null!;
+ if (options.Dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) {
+ OpenGL.DebugContext.Log(LogLevel.Error, $"A main-thread dispatcher is required to create a {nameof(OpenGLRenderer)} when no dispatcher is provided.", prefix: LogId);
+ throw new RequiresMainThreadException(nameof(OpenGLRenderer), "constructor");
+ }
+ Dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ } else {
+ Dispatcher = options.Dispatcher;
+ }
+ // ReSharper disable VirtualMemberCallInConstructor
+ ShouldMeasureFrameRate = options.ShouldMeasureFrameRate;
+ AvgDeltaTime = 0;
+ AvgFrameRate = 0;
+ // ReSharper enable VirtualMemberCallInConstructor
+
+ // Wait for other processes if they are initializing
+ using Mutex mutex = new(true, "Global\\CatalystUI_OpenGL_Lock", out bool newMutex);
+ if (!newMutex) {
+ if (!mutex.WaitOne(ThreadDelegateDispatcher.LockoutTimeout)) {
+ throw new RendererException("Failed to acquire mutex lock for OpenGL renderer initialization.");
+ }
+ }
+ try {
+ // Perform window initialization
+ if (!Dispatcher.Execute(() => {
+ // Request an OpenGL API instance and associated native connector
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Requesting OpenGL instance...");
+ OpenGL = OpenGL.GetInstance(Dispatcher);
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Fetching OpenGL native connector...");
+ // the following line throws if it doesn't exist. we perform checks elsewhere, so if this
+ // fails, it means the end-user constructed the renderer directly and should handle the
+ // error themselves.
+ OpenGLNative = ModelRegistry.RequestConnector>>();
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Done.");
+
+ // Check for a render target
+ if (options.Target != null) {
+ // Set the render target
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"Setting render target {options.Target.GetType().Name}...");
+ SetTarget(options.Target);
+
+ // Set the layers
+ for (int i = 0; i < options.Layers.Length; i++) {
+ IRenderLayer layer = options.Layers[i];
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"Registering layer {layer.GetType().Name}...");
+ TryRegisterLayer(layer);
+ }
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Info, "The OpenGL context cannot be set until a render target is provided. The renderer will be inactive until a target is set.", prefix: LogId);
+ }
+
+ // Fire the initialized handler
+ options.InitializedHandler?.Invoke(this);
+
+ // Fire initial render call
+ Render();
+
+ // Fire created event
+ OnCreated();
+
+ // Attach renderer event loop
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Starting render loop...", prefix: LogId);
+ Dispatcher.PreExecute += _handlePreExecute;
+ Dispatcher.DelegateEnqueued += _handleDelegateEnqueued;
+ }, wait: true, timeout: Timeout.Infinite)) {
+ throw new RendererException("Failed to initialize the renderer.");
+ }
+ } finally {
+ // Release the mutex
+ mutex.ReleaseMutex();
+ }
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~OpenGLRenderer() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public virtual void SetTarget(IRenderTarget? target) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (_target == target) return;
+ _ = Dispatcher.Execute(() => {
+ // Delete the existing context if one exists
+ if (_target != null) {
+ // Detach events
+ _target.Updated -= _handleTargetUpdated;
+ _target.Destroying -= _handleTargetDestroying;
+
+ // Delete the context
+ OpenGL.DebugContext.Log(LogLevel.Info, $"Deleting existing OpenGL context for target {_target.GetType().Name}...", prefix: LogId);
+ if (!OpenGLNative.TryDeleteContext(OpenGLContext, _target)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, "Failed to delete existing OpenGL context.", prefix: LogId);
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "Existing OpenGL context deleted.", prefix: LogId);
+ }
+ OpenGLContext = nint.Zero;
+ }
+
+ // If we don't have a new target to set, we're all done
+ if (target == null) {
+ OpenGL.DebugContext.Log(LogLevel.Info, "No new render target provided; renderer is now inactive.", prefix: LogId);
+ _target = null;
+ return;
+ }
+
+ // Register the new target
+ _target = target;
+
+ // Create and bind the OpenGL context
+ OpenGL.DebugContext.Log(LogLevel.Info, $"Creating OpenGL context for target {target.GetType().Name}...", prefix: LogId);
+ OpenGLContext = OpenGLNative.CreateContext(OpenGL, _target);
+ if (OpenGLContext == 0) throw new RendererException("Failed to create OpenGL context.");
+ if (!OpenGLNative.TryMakeContextCurrent(OpenGLContext, _target)) throw new RendererException("Failed to make OpenGL context current.");
+ OpenGL.DebugContext.Log(LogLevel.Debug, "OpenGL context created and made current.", prefix: LogId);
+
+ // Set the swap interval
+ UseVSync = _useVSync;
+
+ // Determine debug extensions
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Discovering OpenGL extensions...", prefix: LogId);
+ GL gl = OpenGL.Api;
+ int numExt = gl.GetInteger(GLEnum.NumExtensions);
+ for (uint i = 0; i < numExt; i++) {
+ string ext = gl.GetStringS(GLEnum.Extensions, i);
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"OpenGL Extension: {ext}", prefix: LogId);
+ }
+
+ // Mark OpenGL debug info
+ unsafe {
+ if (gl.IsExtensionPresent("GL_ARB_debug_output") || gl.IsExtensionPresent("GL_KHR_debug") || (gl.GetInteger(GLEnum.ContextFlags) & (int) GLEnum.ContextFlagDebugBit) != 0) {
+ gl.Enable(EnableCap.DebugOutput);
+ gl.Enable(EnableCap.DebugOutputSynchronous);
+ // TODO: small memory leak?
+ gl.DebugMessageCallback((source, type, id, severity, length, message, userParam) => {
+ ReadOnlySpan span = new((void*) message, length);
+ string log = Encoding.UTF8.GetString(span);
+ if (severity == (GLEnum) DebugSeverity.DebugSeverityNotification) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"OpenGL: {log}");
+ return;
+ }
+ if (severity == (GLEnum) DebugSeverity.DebugSeverityLow) {
+ OpenGL.DebugContext.Log(LogLevel.Debug, $"OpenGL: {log}");
+ return;
+ }
+ OnErrored(new(log));
+ }, (void*) 0);
+ gl.DebugMessageControl(DebugSource.DontCare, DebugType.DontCare, DebugSeverity.DontCare, 0, null, true);
+ }
+ }
+
+ // Attach events
+ _target.Updated += _handleTargetUpdated;
+ _target.Destroying += _handleTargetDestroying;
+
+ // Check for errors
+ GLEnum err = gl.GetError();
+ if (err != GLEnum.NoError) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, $"OpenGL reported an error when setting the target: {err}", prefix: LogId);
+ }
+
+ // Check framebuffer status
+ GLEnum status = gl.CheckFramebufferStatus(GLEnum.Framebuffer);
+ if (status != GLEnum.FramebufferComplete) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, $"OpenGL framebuffer is not complete: {status}", prefix: LogId);
+ } else {
+ OpenGL.DebugContext.Log(LogLevel.Debug, "OpenGL framebuffer is complete.", prefix: LogId);
+ }
+
+ // Mark first frame
+ _firstFrame = true;
+ }, wait: true);
+ }
+
+ ///
+ public virtual bool TryRegisterLayer(IRenderLayer layer) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ bool result = false;
+ _ = Dispatcher.Execute(() => {
+ if (_layers.Contains(layer)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, $"Layer {layer.GetType().Name} is already registered; skipping...", prefix: LogId);
+ result = false;
+ return;
+ }
+ _layers.Add(layer);
+ OpenGL.DebugContext.Log(LogLevel.Debug, $"Layer {layer.GetType().Name} registered.", prefix: LogId);
+ result = true;
+ }, wait: true);
+ return result;
+ }
+
+ ///
+ public virtual void UnregisterLayer(IRenderLayer layer) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ if (!_layers.Contains(layer)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, $"Layer {layer.GetType().Name} is not registered; cannot unregister.", prefix: LogId);
+ return;
+ }
+ _layers.Remove(layer);
+ OpenGL.DebugContext.Log(LogLevel.Debug, $"Layer {layer.GetType().Name} unregistered.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Render() {
+ if (_disposed) return;
+ if (_target == null) return;
+ if (!_target.IsEnabled) return;
+ if (_target.Width == 0 || _target.Height == 0) return;
+ if (OpenGLContext == nint.Zero) return;
+ if (!OpenGLNative.TryMakeContextCurrent(OpenGLContext, _target)) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Failed to make OpenGL context current; skipping render.", prefix: LogId);
+ return;
+ }
+ GL gl = OpenGL.Api;
+
+ // Frame start
+ OnFrameStart();
+
+ // Update the frame time
+ if (ShouldMeasureFrameRate) {
+ UpdateMeasuredFrameRate();
+ if (DateTime.UtcNow - _frameRateLastReportTime >= TimeSpan.FromSeconds(1)) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"Reported Frame Rate: {AvgFrameRate:F2} FPS, Average Delta Time: {AvgDeltaTime * 1000.0:F2} ms", prefix: LogId);
+ _frameRateLastReportTime = DateTime.UtcNow;
+ }
+ }
+
+ // Update the viewport
+ uint targetWidth = _target.Width;
+ uint targetHeight = _target.Height;
+ if (targetWidth != _previousTargetWidth || targetHeight != _previousTargetHeight) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, $"Updating viewport to {targetWidth}x{targetHeight}...", prefix: LogId);
+ gl.Viewport(0, 0, targetWidth, targetHeight);
+ _previousTargetWidth = targetWidth;
+ _previousTargetHeight = targetHeight;
+ }
+
+ // If there's no layers, clear to white and present
+ // Otherwise, loop through the layers and pass control
+ if (_layers.Count == 0) {
+ gl.ClearColor(DEFAULT_CLEAR_COLOR.R, DEFAULT_CLEAR_COLOR.G, DEFAULT_CLEAR_COLOR.B, DEFAULT_CLEAR_COLOR.A);
+ gl.Clear((uint) ClearBufferMask.ColorBufferBit);
+ } else {
+ for (int i = 0; i < _layers.Count; i++) {
+ try {
+ _layers[i].Render(this);
+ } catch (RenderLayerException e) {
+ OnErrored(new($"Exception occurred while rendering layer {_layers[i].GetType().Name}: {e.Message}{Environment.NewLine}{e.StackTrace}"));
+ } catch (Exception e) {
+ OnErrored(new($"Non-layer exception occurred while rendering layer {_layers[i].GetType().Name}: {e.Message}{Environment.NewLine}{e.StackTrace}"));
+ }
+ }
+ }
+
+ // Check for rendering errors
+ GLEnum err = gl.GetError();
+ if (err != GLEnum.NoError) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, $"OpenGL reported an error during rendering: {err}", prefix: LogId);
+ }
+
+ // Swap the buffers
+ if (!OpenGLNative.TrySwapBuffers(_target)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, "Failed to swap buffers after rendering.", prefix: LogId);
+ }
+
+ // Handle first frame
+ if (_firstFrame) OnFirstFrame();
+
+ // Frame end
+ OnFrameEnd();
+ }
+
+ ///
+ public virtual void Clear(Color128 color, bool depth) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ if (_disposed) return;
+ if (_target is not { IsEnabled: true }) return;
+ if (_target.Width == 0 || _target.Height == 0) return;
+ if (OpenGLContext == nint.Zero) return;
+ if (!OpenGLNative.TryMakeContextCurrent(OpenGLContext, _target)) {
+ OpenGL.DebugContext.Log(LogLevel.Verbose, "Failed to make OpenGL context current; skipping render.", prefix: LogId);
+ return;
+ }
+ GL gl = OpenGL.Api;
+
+ // Frame start
+ OnFrameStart();
+
+ // Clear the buffers
+ gl.Viewport(0, 0, _target.Width, _target.Height);
+ gl.ClearColor(color.R, color.G, color.B, color.A);
+ gl.Clear((uint) (ClearBufferMask.ColorBufferBit | (depth ? ClearBufferMask.DepthBufferBit : 0)));
+ gl.Flush();
+
+ // Swap the buffers
+ if (!OpenGLNative.TrySwapBuffers(_target)) {
+ OpenGL.DebugContext.Log(LogLevel.Warning, "Failed to swap buffers after clearing.", prefix: LogId);
+ }
+
+ // Frame end
+ OnFrameEnd();
+ }, wait: true);
+ }
+
+ ///
+ /// Updates the measured frame rate and delta time.
+ ///
+ protected virtual void UpdateMeasuredFrameRate() {
+ // Update frame time history
+ if (_frameRateSamples.IsFull) _frameRateSamples.Dequeue();
+ ref double lastFrameTime = ref _frameRateSamples.PeekEnqueue();
+ lastFrameTime = (double) _frameRateStopWatch.ElapsedTicks / Stopwatch.Frequency;
+ _frameRateSamples.Enqueue();
+
+ // Apply exponential smoothing
+ if (AvgDeltaTime <= 0.0) AvgDeltaTime = lastFrameTime;
+ else AvgDeltaTime = AvgDeltaTime * (1.0 - 0.1) + lastFrameTime * 0.1;
+
+ // Calculate the average frame-rate
+ double fpsSum = 0.0;
+ ushort fpsCount = 0;
+ double[] samples = _frameRateSamples.Items;
+ for (int i = 0; i < samples.Length; i++) {
+ double sample = samples[i];
+ if (sample >= 0.0) {
+ fpsSum += sample;
+ fpsCount++;
+ }
+ }
+ AvgFrameRate = fpsCount > 0 && fpsSum > 0 ? 1.0 / (fpsSum / fpsCount) : 0.0;
+
+ // Restart the stopwatch
+ _frameRateStopWatch.Restart();
+ }
+
+ ///
+ protected virtual void HandlePreExecute(ThreadDelegateDispatcher dispatcher) {
+ if (_targetFrameRate == 0) return; // If passive rendering, no need to handle
+ if (_targetFrameRate != ushort.MaxValue) {
+ // Perform a render and calculate render time
+ long startTicks = Stopwatch.GetTimestamp();
+ Render();
+ long endTicks = Stopwatch.GetTimestamp();
+
+ // Calculate target ticks per frame, then calculate remaining ticks
+ long targetTicks = Stopwatch.Frequency / _targetFrameRate;
+ long elapsedTicks = endTicks - startTicks;
+ long remainingTicks = targetTicks - elapsedTicks;
+
+ // If rendering didn't take the full time, wait the remaining time
+ if (remainingTicks > 0) {
+ // Determine a rough ms value and wait for that
+ int remainingMs = (int) Math.Max((remainingTicks * 1000L / Stopwatch.Frequency) - PRECISE_TIMING_BUFFER, 0);
+ if (remainingMs > 0) {
+ _resetPollEventHandle.WaitOne(remainingMs);
+ if (_resetPollEventHandleState) {
+ _resetPollEventHandle.Reset();
+ _resetPollEventHandleState = false;
+ }
+ }
+
+ // Then wait using a more precise method
+ remainingTicks = (targetTicks + startTicks) - Stopwatch.GetTimestamp();
+ if (remainingTicks > 0) {
+ long spinTicks = Stopwatch.GetTimestamp() + remainingTicks;
+ while (!_resetPollEventHandleState) {
+ long now = Stopwatch.GetTimestamp();
+ if (now >= spinTicks) break;
+ long ticksLeft = spinTicks - now;
+ if (ticksLeft > Stopwatch.Frequency * 0.001) { // > 1ms
+ Thread.SpinWait(50);
+ } else if (ticksLeft > Stopwatch.Frequency * 0.0005) { // 0.5–1ms
+ Thread.Yield();
+ } else {
+ // hot spin
+ }
+ }
+ }
+ }
+
+ // Reset the poll event if it was set
+ if (_resetPollEventHandleState) {
+ _resetPollEventHandle.Reset();
+ _resetPollEventHandleState = false;
+ }
+ } else {
+ Render();
+ }
+ }
+
+ ///
+ protected virtual void HandleDelegateEnqueued(ThreadDelegateDispatcher dispatcher, Delegate @delegate) {
+ _resetPollEventHandle.Set();
+ _resetPollEventHandleState = true;
+ }
+
+ ///
+ protected virtual void HandleTargetUpdated(IRenderTarget target) {
+ // TODO: Linux check (it doesn't like us blocking the window thread for rendering...)
+ _ = Dispatcher.Execute(_cachedActionRender, wait: true);
+ }
+
+ ///
+ protected virtual void HandleTargetDestroying(IRenderTarget target) {
+ SetTarget(null);
+ }
+
+ ///
+ protected virtual void OnErrored(RendererException exception) {
+ Errored?.Invoke(this, exception);
+ }
+
+ ///
+ protected virtual void OnCreated() {
+ Created?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnFrameStart() {
+ FrameStart?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnFrameEnd() {
+ FrameEnd?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnFirstFrame() {
+ _firstFrameHandler?.Invoke(this);
+ _firstFrame = false;
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public virtual void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.EnterWriteLock();
+ try {
+ if (_disposed) return;
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // Detach dispatcher events
+ Dispatcher.PreExecute -= _handlePreExecute;
+ Dispatcher.DelegateEnqueued -= _handleDelegateEnqueued;
+
+ // Dispose render target and OpenGL context
+ if (OpenGLContext != nint.Zero && _target != null) {
+ OpenGLNative.TryDeleteContext(OpenGLContext, _target);
+ }
+ SetTarget(null);
+ OpenGL?.Dispose();
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ // ...
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/CatalystUI.Modules.Crystal.Sdl2.csproj b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/CatalystUI.Modules.Crystal.Sdl2.csproj
new file mode 100644
index 0000000..d8af19a
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/CatalystUI.Modules.Crystal.Sdl2.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Catalyst.Modules.Crystal.Sdl2
+ Catalyst.Modules.Crystal.Sdl2
+ true
+
+
+ CatalystUI Crystal – Sdl2
+ 1.0.0
+ alpha.1
+ CatalystUI LLC
+ Sdl2 API for the Crystal subset of modules provided by the CatalystUI library.
+ CatalystUI,Crystal,sdl2,sdl,ui,gui,graphics,visual,window
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Extensions/CatalystAppBuilderExtensions.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Extensions/CatalystAppBuilderExtensions.cs
new file mode 100644
index 0000000..53b4e03
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Extensions/CatalystAppBuilderExtensions.cs
@@ -0,0 +1,55 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Modules.Crystal.Sdl2;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Builders.Extensions {
+
+ ///
+ /// Builder extensions for the .
+ ///
+ public static class CatalystAppBuilderExtensions {
+
+ ///
+ /// Adds the Crystal-based Sdl2 windowing module to the .
+ ///
+ ///
+ ///
+ /// The Crystal-based Sdl2 windowing module adds the following to your CatalystUI application:
+ ///
+ ///
+ ///
+ ///
+ /// Click on any of the above links to learn more about each component.
+ ///
+ ///
+ /// The to add the module to.
+ /// The with the Sdl2 windowing module added.
+ public static CatalystAppBuilder AddCrystalSdl2Module(this CatalystAppBuilder builder) {
+ Sdl2WindowLayer glfw3WindowingLayer = new();
+ ModelRegistry.RegisterLayer(glfw3WindowingLayer);
+ // if (SystemDetector.IsSystem()) {
+ // throw new NotImplementedException();
+ // } else if (SystemDetector.IsSystem()) {
+ // throw new NotImplementedException();
+ // } else if (SystemDetector.IsSystem()) {
+ // throw new NotImplementedException();
+ // } else {
+ // // Native functionality for Glfw3 is OPTIONAL.
+ // // Do not throw an error here unless it becomes REQUIRED.
+ // }
+ return builder;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2.cs
new file mode 100644
index 0000000..ab722a9
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2.cs
@@ -0,0 +1,211 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Attributes.Threading;
+using Catalyst.Debugging;
+using Catalyst.Native;
+using Catalyst.Threading;
+using Silk.NET.SDL;
+using System;
+using System.Threading;
+
+namespace Catalyst.Modules.Crystal.Sdl2 {
+
+ ///
+ /// Native API wrapper for the Sdl2 library.
+ ///
+ public sealed partial class Sdl2 : INativeApi {
+
+ ///
+ /// Default initialization flags for the Sdl2 API.
+ ///
+ public const uint DEFAULT_INIT_FLAGS =
+ Sdl.InitVideo |
+ Sdl.InitEvents |
+ Sdl.InitTimer |
+ Sdl.InitGamecontroller;
+
+ ///
+ /// The underlying wrapped API instance.
+ ///
+ private static Sdl? _api;
+
+ ///
+ /// The dispatcher which was used to initialize the API.
+ ///
+ private static ThreadDelegateDispatcher? _initDispatcher;
+
+ ///
+ /// The total number of 'requests' or instantiations of the API.
+ ///
+ private static ushort _referenceCount;
+
+ ///
+ /// A static lock used to ensure thread-safe access to the static members.
+ ///
+ private static readonly Lock _staticLock;
+
+ ///
+ /// Gets the Sdl2 debug context.
+ ///
+ /// The debug context for Sdl2.
+ public static DebugContext DebugContext { get; }
+
+ ///
+ /// Gets the wrapped API instance.
+ ///
+ /// The wrapped Sdl2 API instance.
+ public Sdl Api {
+ get {
+ _staticLock.Enter();
+ try {
+ return _api!; // Non-nullable because it is initialized in the static constructor
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+ }
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ private readonly Lock _lock;
+
+ ///
+ /// Static constructor for
+ ///
+ static Sdl2() {
+ // Fields
+ _referenceCount = 0;
+ _staticLock = new();
+
+ // Properties
+ DebugContext = CatalystDebug.ForContext("Sdl2");
+ }
+
+ ///
+ /// Constructs a new .
+ ///
+ private Sdl2() {
+ // Fields
+ _disposed = false;
+ _lock = new();
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~Sdl2() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public static Sdl2 GetInstance(ThreadDelegateDispatcher? dispatcher = null) {
+ _staticLock.Enter();
+ try {
+ if (_referenceCount == 0) Initialize(dispatcher);
+ _referenceCount++;
+ return new();
+ } finally {
+ _staticLock.Exit();
+ }
+ }
+
+ ///
+ /// Initializes the Sdl2 API.
+ ///
+ private static void Initialize(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Sdl2), nameof(GetInstance));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionInitializeUnsafe, wait: true)) {
+ throw new TypeInitializationException(nameof(Sdl2), new InvalidOperationException("Failed to initialize Sdl2 API on the main thread."));
+ }
+ _initDispatcher = dispatcher;
+ }
+
+ [CachedDelegate]
+ private static void InitializeUnsafe() {
+ if (_api != null) throw new InvalidOperationException("The Sdl2 API is already initialized.");
+ _api = Sdl.GetApi();
+ _api.Init(DEFAULT_INIT_FLAGS);
+ if ((_api.WasInit(DEFAULT_INIT_FLAGS) & DEFAULT_INIT_FLAGS) != DEFAULT_INIT_FLAGS) {
+ throw new WindowException("Sdl2 failed to initialize required subsystems.");
+ }
+ }
+
+ ///
+ /// Terminates the Sdl2 API.
+ ///
+ private static void Terminate(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Sdl2), nameof(Terminate));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedActionTerminateUnsafe, wait: true)) {
+ throw new InvalidOperationException("Failed to terminate Sdl2 API on the main thread.");
+ }
+ }
+
+ [CachedDelegate]
+ private static void TerminateUnsafe() {
+ _api?.Quit();
+ _api?.Dispose();
+ _api = null;
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.Enter();
+ try {
+ if (_disposed) return;
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // ...
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ _staticLock.Enter();
+ try {
+ _referenceCount--;
+ if (_referenceCount == 0) Terminate(_initDispatcher);
+ } finally {
+ _staticLock.Exit();
+ }
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.Exit();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2WindowLayer.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2WindowLayer.cs
new file mode 100644
index 0000000..94e9f56
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Sdl2WindowLayer.cs
@@ -0,0 +1,75 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Attributes.Threading;
+using Catalyst.Threading;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Catalyst.Modules.Crystal.Sdl2 {
+
+ ///
+ /// The Crystal implementation of the CatalystUI type
+ /// using the Sdl2 windowing API.
+ ///
+ public sealed unsafe partial class Sdl2WindowLayer : IWindowLayer {
+
+ ///
+ public IEnumerable GetDisplays(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Sdl2WindowLayer), nameof(GetDisplays));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedFunctionGetDisplaysUnsafe, dispatcher, out SdlDisplay[] displays)) {
+ throw new WindowException("Failed to get displays from Sdl2 on the main thread.");
+ }
+ return displays.OfType();
+ }
+
+ [CachedDelegate]
+ internal static SdlDisplay[] GetDisplaysUnsafe(ThreadDelegateDispatcher dispatcher) {
+ using Sdl2 sdl = Sdl2.GetInstance(dispatcher);
+ int count = sdl.Api.GetNumVideoDisplays();
+ if (count == 0) return [];
+ SdlDisplay[] displays = new SdlDisplay[count];
+ for (int i = 0; i < count; i++) {
+ displays[i] = SdlDisplay.FromIndex(sdl, i);
+ }
+ return displays;
+ }
+
+ ///
+ public IDisplay? GetPrimaryDisplay(ThreadDelegateDispatcher? dispatcher = null) {
+ if (dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) throw new RequiresMainThreadException(nameof(Sdl2WindowLayer), nameof(GetPrimaryDisplay));
+ dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ }
+ if (!dispatcher.Execute(_cachedFunctionGetPrimaryDisplayUnsafe, dispatcher, out SdlDisplay? display)) {
+ throw new WindowException("Failed to get primary display from Sdl2 on the main thread.");
+ }
+ return display;
+ }
+
+ [CachedDelegate]
+ internal static SdlDisplay? GetPrimaryDisplayUnsafe(ThreadDelegateDispatcher dispatcher) {
+ using Sdl2 sdl = Sdl2.GetInstance(dispatcher);
+ SdlDisplay display = SdlDisplay.FromIndex(sdl, 0);
+ return display;
+ }
+
+ ///
+ public IWindow CreateWindow(WindowOptions? options = null) {
+ return new SdlWindow(options ?? new());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlDisplay.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlDisplay.cs
new file mode 100644
index 0000000..e003dab
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlDisplay.cs
@@ -0,0 +1,132 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Debugging;
+using Catalyst.Mathematics.Geometry;
+using Silk.NET.Maths;
+using Silk.NET.SDL;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Sdl2 {
+
+ ///
+ /// An implementation of using the Sdl2 windowing API.
+ ///
+ public readonly record struct SdlDisplay : IDisplay {
+
+ ///
+ /// Gets the Sdl2 display index for the display.
+ ///
+ /// The display's Sdl2 index.
+ public required int Index { get; init; }
+
+ ///
+ public required string Descriptor { get; init; }
+
+ ///
+ public required string? Manufacturer { get; init; }
+
+ ///
+ public required double RefreshRate { get; init; }
+
+ ///
+ public required double X { get; init; }
+
+ ///
+ public required double Y { get; init; }
+
+ ///
+ public required uint Width { get; init; }
+
+ ///
+ public required uint Height { get; init; }
+
+ ///
+ public required Angle Rotation { get; init; }
+
+ ///
+ public required DisplayOrientation Orientation { get; init; }
+
+ ///
+ public required double PixelsPerInch { get; init; }
+
+ ///
+ public required double ScalingFactor { get; init; }
+
+ ///
+ /// Constructs a new from the specified index.
+ ///
+ /// The Sdl2 API instance.
+ /// The index of the display to construct from.
+ /// A new instance of .
+ public static unsafe SdlDisplay FromIndex(Sdl2 sdl, int index) {
+ // TODO: Native display names and manufacturers if possible
+ // TODO: Native angle, orientation
+
+ // Parse display mode
+ DisplayMode mode = default;
+ sdl.Api.GetDesktopDisplayMode(index, ref mode);
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Parsed display mode for display {index}: {mode.W}x{mode.H} @ {mode.RefreshRate}Hz, format {mode.Format}");
+
+ // Parse display bounds
+ Rectangle bounds = default;
+ sdl.Api.GetDisplayBounds(index, ref bounds);
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Parsed display bounds for display {index}: Position ({bounds.Origin.X}, {bounds.Origin.Y}), Size ({bounds.Size.X}x{bounds.Size.Y})");
+
+ // Parse display orientation
+ Silk.NET.SDL.DisplayOrientation sdlOrientation = sdl.Api.GetDisplayOrientation(index);
+ DisplayOrientation orientation = sdlOrientation switch {
+ Silk.NET.SDL.DisplayOrientation.Landscape => DisplayOrientation.Landscape,
+ Silk.NET.SDL.DisplayOrientation.LandscapeFlipped => DisplayOrientation.LandscapeFlipped,
+ Silk.NET.SDL.DisplayOrientation.Portrait => DisplayOrientation.Portrait,
+ Silk.NET.SDL.DisplayOrientation.PortraitFlipped => DisplayOrientation.PortraitFlipped,
+ _ => DisplayOrientation.Landscape
+ };
+ Angle rotation = sdlOrientation switch {
+ Silk.NET.SDL.DisplayOrientation.Landscape => Angle.FromDegrees(0),
+ Silk.NET.SDL.DisplayOrientation.LandscapeFlipped => Angle.FromDegrees(180),
+ Silk.NET.SDL.DisplayOrientation.Portrait => Angle.FromDegrees(270),
+ Silk.NET.SDL.DisplayOrientation.PortraitFlipped => Angle.FromDegrees(90),
+ _ => Angle.FromDegrees(0)
+ };
+
+ // Parse display DPI
+ float ddpi = 0.0f;
+ float hdpi = 0.0f;
+ float vdpi = 0.0f;
+ sdl.Api.GetDisplayDPI(index, ref ddpi, ref hdpi, ref vdpi);
+ double ppi = (hdpi + vdpi) / 2.0;
+ double scale = ppi / 96.0;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Parsed display DPI for display {index}: Horizontal DPI {hdpi}, Vertical DPI {vdpi}, Average PPI {ppi}, Scaling Factor {scale}");
+
+ // Parse display descriptor
+ string descriptor = sdl.Api.GetDisplayNameS(index);
+
+ // Construct and return
+ return new() {
+ Index = index,
+ Descriptor = descriptor,
+ Manufacturer = "",
+ RefreshRate = mode.RefreshRate,
+ X = bounds.Origin.X,
+ Y = bounds.Origin.Y,
+ Width = (uint) bounds.Size.X,
+ Height = (uint) bounds.Size.Y,
+ Rotation = rotation,
+ Orientation = orientation,
+ PixelsPerInch = ppi,
+ ScalingFactor = scale
+ };
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlWindow.cs b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlWindow.cs
new file mode 100644
index 0000000..5983190
--- /dev/null
+++ b/CatalystUI/Modules/Crystal/CatalystUI.Modules.Crystal.Sdl2/Window/SdlWindow.cs
@@ -0,0 +1,1004 @@
+// -------------------------------------------------------------------------------------------------
+// CatalystUI Framework for .NET Core - https://catalystui.org/
+// Copyright (c) 2025 CatalystUI LLC. All rights reserved.
+//
+// This file is part of CatalystUI and is provided as part of an early-access release.
+// Unauthorized commercial use, distribution, or modification is strictly prohibited.
+//
+// This software is not open source and is not publicly licensed.
+// For full terms, see the LICENSE and NOTICE files in the project root.
+// -------------------------------------------------------------------------------------------------
+
+using Catalyst.Debugging;
+using Catalyst.Supplementary;
+using Catalyst.Threading;
+using Silk.NET.Core.Native;
+using Silk.NET.SDL;
+using System;
+using System.Linq;
+using System.Threading;
+using Mutex = System.Threading.Mutex;
+
+// ReSharper disable once CheckNamespace
+namespace Catalyst.Modules.Crystal.Sdl2 {
+
+ ///
+ /// An implementation of using
+ /// the Sdl2 windowing API.
+ ///
+ public unsafe class SdlWindow : IWindow {
+
+ ///
+ /// The minimum polling rate for the window in milliseconds.
+ ///
+ ///
+ /// Prevents lockups of the main thread when the
+ /// window may not be responding.
+ ///
+ public const ushort MINIMUM_POLL_RATE = 3000;
+
+ ///
+ public event IWindow.WindowErroredEventHandler? Errored;
+
+ ///
+ public event IWindow.WindowEventHandler? Created;
+
+ ///
+ public event IWindow.WindowEventHandler? Repositioned;
+
+ ///
+ public event IWindow.WindowEventHandler? Resized;
+
+ ///
+ public event IWindow.WindowEventHandler? Refresh;
+
+ ///
+ public event IWindow.WindowEventHandler? Redraw;
+
+ ///
+ public event IWindow.WindowEventHandler? Focused;
+
+ ///
+ public event IWindow.WindowEventHandler? Unfocused;
+
+ ///
+ public event IWindow.WindowEventHandler? Minimized;
+
+ ///
+ public event IWindow.WindowEventHandler? Maximized;
+
+ ///
+ public event IWindow.WindowEventHandler? Restored;
+
+ ///
+ public event IWindow.WindowEventHandler? Shown;
+
+ ///
+ public event IWindow.WindowEventHandler? Hidden;
+
+ ///
+ public event IWindow.WindowClosingEventHandler? Closing;
+
+ ///
+ public event IWindow.WindowEventHandler? Disposing;
+
+ ///
+ public event IWindow.WindowEventHandler? Closed;
+
+ ///
+ public event IWindow.WindowInteractedEventHandler? Interacted;
+
+ ///
+ /// Gets a generated log identifier for the window.
+ ///
+ /// The window class name and native handle in hexadecimal format.
+ protected string LogId => $"{nameof(SdlWindow)} {(NativeHandle.Length > 0 ? $"0x{NativeHandle[0]:X}" : "(No Native Handle)")}";
+
+ ///
+ /// Gets or sets the Sdl2 instance associated with the window.
+ ///
+ /// The Sdl2 instance.
+ public Sdl2 Sdl { get; private set; }
+
+ ///
+ /// Gets or sets the Sdl2 window handle.
+ ///
+ /// The Sdl2 window handle.
+ public nint SdlHandle { get; private set; }
+
+ ///
+ /// Gets or sets the Sdl2 window ID.
+ ///
+ /// The Sdl2 window ID.
+ public uint SdlId { get; private set; }
+
+ ///
+ public ThreadDelegateDispatcher Dispatcher { get; init; }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected nint[] _nativeHandle;
+
+ ///
+ public nint[] NativeHandle => _nativeHandle;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile ushort _pollRate;
+
+ ///
+ public virtual ushort PollRate {
+ get => _pollRate;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _pollRate = value; // set outside dispatcher to avoid deadlock
+ _ = Dispatcher.Execute(() => {
+ // simply enqueing a no-op will cause the poll rate to
+ // be re-evaluated due to the polling loop
+ }, wait: true);
+ }
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile IDisplay? _display;
+
+ ///
+ public virtual IDisplay? Display => _display;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile string _title;
+
+ ///
+ public virtual string Title {
+ get => _title;
+ set {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ byte* titlePtr = (byte*) SilkMarshal.StringToPtr(value);
+ Sdl.Api.SetWindowTitle((Window*) SdlHandle, titlePtr);
+ SilkMarshal.Free((nint) titlePtr);
+ _title = value;
+ Sdl2.DebugContext.Log(LogLevel.Debug, $"Window title set to '{value}'.", prefix: LogId);
+ }, wait: true);
+ }
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected double _x;
+
+ ///
+ public virtual double X {
+ get => Volatile.Read(ref _x);
+ set => SetPosition(value, Y);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected double _y;
+
+ ///
+ public virtual double Y {
+ get => Volatile.Read(ref _y);
+ set => SetPosition(X, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _minimumWidth;
+
+ ///
+ public virtual uint MinimumWidth {
+ get => _minimumWidth;
+ set => SetSizeLimits(value, MinimumHeight, MaximumWidth, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _width;
+
+ ///
+ public virtual uint Width {
+ get => _width;
+ set => SetSize(value, Height);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _maximumWidth;
+
+ ///
+ public virtual uint MaximumWidth {
+ get => _maximumWidth;
+ set => SetSizeLimits(MinimumWidth, MinimumHeight, value, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _minimumHeight;
+
+ ///
+ public virtual uint MinimumHeight {
+ get => _minimumHeight;
+ set => SetSizeLimits(MinimumWidth, value, MaximumWidth, MaximumHeight);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _height;
+
+ ///
+ public virtual uint Height {
+ get => _height;
+ set => SetSize(Width, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile uint _maximumHeight;
+
+ ///
+ public virtual uint MaximumHeight {
+ get => _maximumHeight;
+ set => SetSizeLimits(MinimumWidth, MinimumHeight, MaximumWidth, value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile WindowFullscreenMode _fullscreenMode;
+
+ ///
+ public virtual WindowFullscreenMode FullscreenMode {
+ get => _fullscreenMode;
+ set => SetFullscreen(value);
+ }
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isResizable;
+
+ ///
+ public virtual bool IsResizable => _isResizable;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isDecorated;
+
+ ///
+ public virtual bool IsDecorated => _isDecorated;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isFocused;
+
+ ///
+ public virtual bool IsFocused => _isFocused;
+
+ ///
+ public virtual bool IsUnfocused => !_isFocused;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isMinimized;
+
+ ///
+ public virtual bool IsMinimized => _isMinimized;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isMaximized;
+
+ ///
+ public virtual bool IsMaximized => _isMaximized;
+
+ ///
+ /// Internal reference for .
+ ///
+ protected volatile bool _isVisible;
+
+ ///
+ public virtual bool IsVisible => _isVisible;
+
+ ///
+ public virtual bool IsHidden => !_isVisible;
+
+ ///
+ public virtual bool IsClosed => _disposed;
+
+ ///
+ /// Used to cache the list of known displays.
+ ///
+ protected SdlDisplay[] _cachedDisplays;
+
+ ///
+ /// Used to cache the primary display.
+ ///
+ protected SdlDisplay? _cachedPrimaryDisplay;
+
+ ///
+ /// Used to signal polling resets.
+ ///
+ protected readonly ManualResetEvent _resetPollEventHandle;
+
+ ///
+ /// Used to track the current state of the poll event handle.
+ ///
+ protected bool _resetPollEventHandleState;
+
+ // Cached delegates for callbacks for performance
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ protected readonly ThreadDelegateDispatcher.DispatcherEventHandler _handlePreExecute;
+ protected readonly ThreadDelegateDispatcher.DispatcherQueueEventHandler _handleDelegateEnqueued;
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+
+ ///
+ /// A flag indicating whether the object has been disposed of.
+ ///
+ private bool _disposed;
+
+ ///
+ /// A lock used to ensure thread-safe access to the object.
+ ///
+ private readonly ReaderWriterLockSlim _lock;
+
+ ///
+ /// Constructs a new with
+ /// the specified options.
+ ///
+ /// The options to use when creating the window.
+ public SdlWindow(WindowOptions options) {
+ // Fields
+ _nativeHandle = [];
+ _pollRate = options.PollRate;
+ _display = null;
+ _title = options.Title;
+ _x = 0;
+ _y = 0;
+ _minimumWidth = uint.MinValue;
+ _width = options.Width;
+ _maximumWidth = uint.MaxValue;
+ _minimumHeight = uint.MinValue;
+ _height = options.Height;
+ _maximumHeight = uint.MaxValue;
+ _fullscreenMode = options.FullscreenMode;
+ _isResizable = options.Resizable;
+ _isDecorated = options.Decorated;
+ _isFocused = false;
+ _isMinimized = false;
+ _isMaximized = false;
+ _isVisible = !options.Hidden;
+ _cachedDisplays = [];
+ _cachedPrimaryDisplay = null;
+ _handlePreExecute = HandlePreExecute;
+ _handleDelegateEnqueued = HandleDelegateEnqueued;
+ _resetPollEventHandle = new(false);
+ _resetPollEventHandleState = false;
+ _disposed = false;
+ _lock = new(LockRecursionPolicy.SupportsRecursion);
+
+ // Properties
+ Sdl = null!;
+ SdlHandle = nint.Zero;
+ SdlId = 0;
+ if (options.Dispatcher == null) {
+ if (!ThreadDelegateDispatcher.IsMainThreadCaptured) {
+ Sdl2.DebugContext.Log(LogLevel.Error, $"A main-thread dispatcher is required to create a {nameof(SdlWindow)} when no dispatcher is provided.", prefix: LogId);
+ throw new RequiresMainThreadException(nameof(SdlWindow), "constructor");
+ }
+ Dispatcher = ThreadDelegateDispatcher.MainThreadDispatcher;
+ } else {
+ Dispatcher = options.Dispatcher;
+ }
+
+ // Wait for other processes if they are initializing
+ using Mutex mutex = new(true, "Global\\CatalystUI_Sdl2_Lock", out bool newMutex);
+ if (!newMutex) {
+ if (!mutex.WaitOne(ThreadDelegateDispatcher.LockoutTimeout)) {
+ throw new WindowException("Failed to acquire mutex lock for Sdl2 window initialization.");
+ }
+ }
+ try {
+ // Perform window initialization
+ if (!Dispatcher.Execute(() => {
+ // Request an Sdl2 API instance
+ Sdl2.DebugContext.Log(LogLevel.Verbose, "Requesting Sdl2 instance...");
+ Sdl = Sdl2.GetInstance(Dispatcher);
+ Sdl sdl = Sdl.Api;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, "Done.");
+
+ // Assign cached displays to avoid multiple creations/destruction of the API
+ _cachedPrimaryDisplay = Sdl2WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(Dispatcher);
+ _cachedDisplays = Sdl2WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher);
+
+ // Specify initial visibility
+ // TODO: Wayland (Linux) check.
+ WindowFlags flags = WindowFlags.None;
+ if (options.Hidden) flags |= WindowFlags.Hidden;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowFlags.Hidden} window hint to {options.Hidden}");
+ if (options.Resizable) flags |= WindowFlags.Resizable;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowFlags.Resizable} window hint to {options.Resizable}");
+ if (!options.Decorated) flags |= WindowFlags.Borderless;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Setting {WindowFlags.Borderless} window hint to {!options.Decorated}");
+
+ // Fire the initialization handler
+ options.InitializedHandler?.Invoke(this);
+
+ // Create the window
+ Sdl2.DebugContext.Log(LogLevel.Verbose, "Creating the window...");
+ byte* titlePtr = (byte*) SilkMarshal.StringToPtr(_title);
+ Window* handle = sdl.CreateWindow(titlePtr, Silk.NET.SDL.Sdl.WindowposUndefined, Silk.NET.SDL.Sdl.WindowposUndefined, (int) _width, (int) _height, (uint) flags);
+ SilkMarshal.Free((nint) titlePtr);
+ SdlHandle = (nint) handle;
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Sdl2 window created with {nameof(SdlHandle)} 0x{SdlHandle:X}.");
+
+ // Fetch the Window ID
+ SdlId = sdl.GetWindowID((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Sdl2 window ID determined: {SdlId}.");
+
+ // Get the native handle(s)
+ SysWMInfo wmInfo = default;
+ sdl.GetVersion(ref wmInfo.Version);
+ if (!sdl.GetWindowWMInfo((Window*) SdlHandle, &wmInfo)) {
+ Sdl2.DebugContext.Log(LogLevel.Warning, "Failed to get native window handle information from Sdl2. Native windowing functionality will be limited, and renderer compatibility may be null.", prefix: LogId);
+ } else {
+ if (SystemDetector.IsSystem()) {
+ _nativeHandle = [ wmInfo.Info.Win.Hwnd, wmInfo.Info.Win.HInstance, wmInfo.Info.Win.HDC ];
+ } else if (SystemDetector.IsSystem()) {
+ _nativeHandle = [ (nint) wmInfo.Info.Cocoa.Window ];
+ } else if (SystemDetector.IsSystem()) {
+ if (wmInfo.Subsystem == SysWMType.X11) {
+ _nativeHandle = [ (nint) wmInfo.Info.X11.Window, (nint) wmInfo.Info.X11.Display ];
+ } else if (wmInfo.Subsystem == SysWMType.Wayland) {
+ _nativeHandle = [ (nint) wmInfo.Info.Wayland.Surface, (nint) wmInfo.Info.Wayland.Display ];
+ } else {
+ Sdl2.DebugContext.Log(LogLevel.Warning, "Unknown Linux windowing subsystem detected. Native windowing functionality will be limited, and renderer compatibility may be null.", prefix: LogId);
+ }
+ } else {
+ Sdl2.DebugContext.Log(LogLevel.Warning, "No native handle found for Sdl2. Native windowing functionality will be limited, and renderer compatibility may be null.", prefix: LogId);
+ }
+ }
+
+ // Set initial window size limits
+ SetSizeLimits(_minimumWidth, _minimumHeight, _maximumWidth, _maximumHeight);
+
+ // Set initial fullscreen mode
+ //SetFullscreen(_fullscreenMode);
+
+ // Set window icons
+ //if (options.Icons is { Length: > 0 }) SetIcons(options.Icons);
+
+ // Fire created
+ OnCreated();
+
+ // Attach dispatcher event loop
+ Sdl2.DebugContext.Log(LogLevel.Verbose, "Starting event loop...", prefix: LogId);
+ Dispatcher.PreExecute += _handlePreExecute;
+ Dispatcher.DelegateEnqueued += _handleDelegateEnqueued;
+ }, wait: true, timeout: Timeout.Infinite)) {
+ throw new WindowException("Failed to initialize the window.");
+ }
+ } finally {
+ // Release the mutex
+ mutex.ReleaseMutex();
+ }
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ ~SdlWindow() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: false);
+ }
+
+ ///
+ public virtual void SetPosition(double x, double y) {
+ // TODO: Wayland (Linux) check.
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.SetWindowPosition((Window*) SdlHandle, (int) x, (int) y);
+ Sdl2.DebugContext.Log(LogLevel.Debug, $"Window position set to ({x}, {y}).", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetSize(uint width, uint height) {
+ // TODO: Wayland (Linux) check.
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (width == 0) width = IWindow.DEFAULT_WIDTH;
+ if (height == 0) height = IWindow.DEFAULT_HEIGHT;
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.SetWindowSize((Window*) SdlHandle, (int) width, (int) height);
+ Sdl2.DebugContext.Log(LogLevel.Debug, $"Window size set to {width}x{height}.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetSizeLimits(uint minWidth, uint minHeight, uint maxWidth, uint maxHeight) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (minWidth == 0) minWidth = uint.MinValue;
+ if (minHeight == 0) minHeight = uint.MinValue;
+ if (maxWidth == 0) maxWidth = uint.MaxValue;
+ if (maxHeight == 0) maxHeight = uint.MaxValue;
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.SetWindowMinimumSize((Window*) SdlHandle, (int) minWidth, (int) minHeight);
+ Sdl.Api.SetWindowMaximumSize((Window*) SdlHandle, (int) maxWidth, (int) maxHeight);
+ Sdl2.DebugContext.Log(LogLevel.Debug, $"Window size limits set to {(_minimumWidth == uint.MinValue ? "Unlimited" : _minimumWidth)}x{(_minimumHeight == uint.MinValue ? "Unlimited" : _minimumHeight)} - {(_maximumWidth == uint.MaxValue ? "Unlimited" : _maximumWidth)}x{(_maximumHeight == uint.MaxValue ? "Unlimited" : _maximumHeight)}.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void SetFullscreen(WindowFullscreenMode mode, IDisplay? display = null, uint width = 0, uint height = 0) {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public virtual void SetIcons(params WindowIcon[] icons) {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public virtual void RequestFocus() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.RaiseWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window focus requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+ /// The flash operation to perform.
+ ///
+ public virtual void RequestAttention(FlashOperation operation) {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.FlashWindow((Window*) SdlHandle, operation);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window attention requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+
+ ///
+ public virtual void RequestAttention() {
+ RequestAttention(FlashOperation.UntilFocused);
+ }
+
+ ///
+ public virtual void Minimize() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.MinimizeWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Maximize() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.MaximizeWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Restore() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.RestoreWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window restored.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Show() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.ShowWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Hide() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Sdl.Api.HideWindow((Window*) SdlHandle);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Close() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Event evt = new() {
+ Type = (uint) EventType.Windowevent,
+ Window = new() {
+ Type = (uint) EventType.Windowevent,
+ Event = (byte) WindowEventID.Close,
+ WindowID = SdlId
+ }
+ };
+ Sdl.Api.PushEvent(&evt);
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window close requested.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Exit() {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _ = Dispatcher.Execute(() => {
+ Dispose();
+ OnClosed();
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window exited.", prefix: LogId);
+ }, wait: true);
+ }
+
+ ///
+ public virtual void Wait() {
+ if (_disposed) return;
+ ManualResetEvent reset = new(false);
+ try {
+ Closed += _ => {
+ // ReSharper disable once AccessToDisposedClosure
+ reset.Set();
+ };
+ reset.WaitOne();
+ } finally {
+ reset.Dispose();
+ }
+ }
+
+ ///
+ /// Refreshes the backing fields for the window properties.
+ ///
+ ///
+ /// Since Sdl2 does not provide events for all window property changes,
+ /// the following method is provided to manually refresh all properties
+ /// from the underlying Sdl2 window state. It should be called whenever
+ /// a property change is suspected that may not have triggered an event.
+ ///
+ protected virtual void RefreshProperties() {
+ // TODO: Wayland (Linux) check.
+ // Pull position
+ int x = 0;
+ int y = 0;
+ Sdl.Api.GetWindowPosition((Window*) SdlHandle, ref x, ref y);
+ _x = x;
+ _y = y;
+
+ // Pull size
+ int width = 0;
+ int height = 0;
+ Sdl.Api.GetWindowSize((Window*) SdlHandle, ref width, ref height);
+ _width = (uint) width;
+ _height = (uint) height;
+
+ // Pull state
+ WindowFlags flags = (WindowFlags) Sdl.Api.GetWindowFlags((Window*) SdlHandle);
+ _isFocused = (flags & WindowFlags.InputFocus) != 0;
+ _isMinimized = (flags & WindowFlags.Minimized) != 0;
+ _isMaximized = (flags & WindowFlags.Maximized) != 0;
+ _isVisible = (flags & WindowFlags.Shown) != 0;
+
+ // Refresh the display
+ RefreshDisplay();
+
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Refreshed on-request properties: Pos({_x}, {_y}), Size({_width}x{_height}), Focused({_isFocused}), Minimized({_isMinimized}), Maximized({_isMaximized}), Visible({_isVisible}), Display({_display})", prefix: LogId);
+ // The rest of internal variables are state-driven and requested on-demand.
+
+ // Fire refresh
+ OnRefresh();
+ }
+
+ ///
+ /// Refreshes the property by
+ /// determining which display the window is currently on.
+ ///
+ protected virtual void RefreshDisplay() {
+ if (_cachedDisplays.Length == 0) return;
+
+ // Determine which monitor has the most overlap
+ double bestOverlap = 0.0f;
+ SdlDisplay? bestDisplay = null;
+ for (int i = 0; i < _cachedDisplays.Length; i++) {
+ double wx = _x;
+ double wy = _y;
+ uint ww = _width;
+ uint wh = _height;
+ double dx = _cachedDisplays[i].X;
+ double dy = _cachedDisplays[i].Y;
+ uint dw = _cachedDisplays[i].Width;
+ uint dh = _cachedDisplays[i].Height;
+
+ // Determine overlap
+ double overlap =
+ (uint) Math.Max(0, Math.Min(wx + ww, dx + (int) dw) - Math.Max(wx, dx)) *
+ (double) Math.Max(0, Math.Min(wy + wh, dy + (int) dh) - Math.Max(wy, dy));
+
+ // Determine best overlap
+ if (overlap > bestOverlap) {
+ bestOverlap = overlap;
+ bestDisplay = _cachedDisplays[i];
+ }
+ }
+
+ // Assign the best display
+ _display = bestDisplay;
+
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Refreshed display. Determined best display: {_display}", prefix: LogId);
+ }
+
+ ///
+ protected virtual void HandlePreExecute(ThreadDelegateDispatcher dispatcher) {
+ // Nullability check due to race conditions during disposal
+ if (Sdl?.Api == null) return;
+ Event @event = default;
+ if (PollRate == 0) {
+ // Passive polling (only when events are enqueued)
+ while (!_disposed && dispatcher.Enqueued == 0) {
+ Sdl.Api.WaitEventTimeout(ref @event, MINIMUM_POLL_RATE);
+ HandleEvent(@event);
+ }
+ } else if (PollRate != ushort.MaxValue) {
+ // Active polling (on X intervals of time)
+ if (Sdl.Api.PollEvent(ref @event) == 1) HandleEvent(@event);
+ _resetPollEventHandle.WaitOne(PollRate);
+ if (_resetPollEventHandleState) {
+ _resetPollEventHandle.Reset();
+ _resetPollEventHandleState = false;
+ }
+ } else {
+ // Fastest polling (on every loop of the main thread)
+ if (Sdl.Api.PollEvent(ref @event) == 1) HandleEvent(@event);
+ }
+ }
+
+ ///
+ protected virtual void HandleDelegateEnqueued(ThreadDelegateDispatcher dispatcher, Delegate @delegate) {
+ Event dummyEvent = new() {
+ Type = (uint) EventType.Userevent,
+ User = new() {
+ Type = (uint) EventType.Userevent,
+ Code = 0x01,
+ Data1 = (void*) 0,
+ Data2 = (void*) 0
+ }
+ };
+ Sdl.Api.PushEvent(&dummyEvent); // un-block the main thread if waiting
+ _resetPollEventHandle.Set();
+ _resetPollEventHandleState = true;
+ }
+
+ ///
+ /// Handles an event received from Sdl2.
+ ///
+ /// The event to handle.
+ protected virtual void HandleEvent(Event evt) {
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Handling Sdl2 event of type {(EventType) evt.Type}.", prefix: LogId);
+ if (evt.Type == (uint) EventType.Displayevent) {
+ DisplayEvent disevt = evt.Display;
+ if (disevt.Event == (byte) DisplayEventID.Moved || disevt.Event == (byte) DisplayEventID.Orientation) {
+ RefreshDisplay();
+ } else if (disevt.Event == (byte) DisplayEventID.Connected || disevt.Event == (byte) DisplayEventID.Disconnected) {
+ // Update cached displays
+ _cachedPrimaryDisplay = Sdl2WindowLayer._cachedFunctionGetPrimaryDisplayUnsafe(Dispatcher);
+ _cachedDisplays = Sdl2WindowLayer._cachedFunctionGetDisplaysUnsafe(Dispatcher);
+ }
+ } else if (evt.Type == (uint) EventType.Windowevent) {
+ WindowEvent winevt = evt.Window;
+ if (winevt.WindowID != SdlId) return; // it's not for us
+ if (winevt.Event == (byte) WindowEventID.Shown) {
+ OnShown();
+ } else if (winevt.Event == (byte) WindowEventID.Hidden) {
+ OnHidden();
+ } else if (winevt.Event == (byte) WindowEventID.Exposed) {
+ RefreshProperties();
+ OnRedraw();
+ } else if (winevt.Event == (byte) WindowEventID.Moved) {
+ RefreshProperties();
+ OnRepositioned();
+ } else if (winevt.Event is (byte) WindowEventID.Resized or (byte) WindowEventID.SizeChanged) { // but why are there two events for this?
+ OnResized();
+ } else if (winevt.Event == (byte) WindowEventID.Minimized) {
+ RefreshProperties();
+ OnMinimized();
+ } else if (winevt.Event == (byte) WindowEventID.Maximized) {
+ RefreshProperties();
+ OnMaximized();
+ } else if (winevt.Event == (byte) WindowEventID.Restored) {
+ RefreshProperties();
+ OnRestored();
+ } else if (winevt.Event == (byte) WindowEventID.FocusGained) {
+ RefreshProperties();
+ OnFocused();
+ } else if (winevt.Event == (byte) WindowEventID.FocusLost) {
+ RefreshProperties();
+ OnUnfocused();
+ } else if (winevt.Event == (byte) WindowEventID.Close) {
+ bool shouldCancel = false;
+ _ = Dispatcher.Execute(() => {
+ shouldCancel = OnClosing();
+ }, wait: true);
+ if (shouldCancel) return;
+ Dispose();
+ OnClosed();
+ }
+ } else if (evt.Type == (uint) EventType.Quit) {
+ bool shouldCancel = false;
+ _ = Dispatcher.Execute(() => {
+ shouldCancel = OnClosing();
+ }, wait: true);
+ if (shouldCancel) return;
+ Dispose();
+ OnClosed();
+ }
+ }
+
+ ///
+ protected virtual void OnErrored(WindowException exception) {
+ Sdl2.DebugContext.Log(LogLevel.Error, "Window exception occurred!", prefix: LogId, args: exception);
+ Errored?.Invoke(this, exception);
+ }
+
+ ///
+ protected virtual void OnCreated() {
+ Sdl2.DebugContext.Log(LogLevel.Info, "Window created.", prefix: LogId);
+ Created?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRepositioned() {
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Window repositioned to ({X}, {Y}).", prefix: LogId);
+ Repositioned?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnResized() {
+ Sdl2.DebugContext.Log(LogLevel.Verbose, $"Window resized to {Width}x{Height}.", prefix: LogId);
+ Resized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRefresh() {
+ Refresh?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRedraw() {
+ Redraw?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnFocused() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window focused.", prefix: LogId);
+ Focused?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnUnfocused() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window unfocused.", prefix: LogId);
+ Unfocused?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnMinimized() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window minimized.", prefix: LogId);
+ Minimized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnMaximized() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window maximized.", prefix: LogId);
+ Maximized?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnRestored() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window restored.", prefix: LogId);
+ Restored?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnShown() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window shown.", prefix: LogId);
+ Shown?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnHidden() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window hidden.", prefix: LogId);
+ Hidden?.Invoke(this);
+ }
+
+ ///
+ protected virtual bool OnClosing() {
+ Sdl2.DebugContext.Log(LogLevel.Debug, "Window closing...", prefix: LogId);
+ Delegate[] delegates = Closing?.GetInvocationList() ?? [];
+ bool result = delegates.Cast().Any(handler => handler(this));
+ if (result) Sdl2.DebugContext.Log(LogLevel.Debug, "Window closure cancelled by event handler.", prefix: LogId);
+ return result;
+ }
+
+ ///
+ protected virtual void OnDisposing() {
+ Sdl2.DebugContext.Log(LogLevel.Info, "Window disposing...", prefix: LogId);
+ Disposing?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnClosed() {
+ Sdl2.DebugContext.Log(LogLevel.Info, "Window closed.", prefix: LogId);
+ Closed?.Invoke(this);
+ }
+
+ ///
+ protected virtual void OnInteracted() {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Disposes of the .
+ ///
+ public virtual void Dispose() {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// if disposal is being performed by the garbage collector, otherwise
+ ///
+ private void Dispose(bool disposing) {
+ _lock.EnterWriteLock();
+ try {
+ if (_disposed) return;
+
+ // Disposing
+ OnDisposing();
+
+ // Dispose managed state (managed objects)
+ if (disposing) {
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (Dispatcher != null) {
+ Dispatcher.PreExecute -= _handlePreExecute;
+ Dispatcher.DelegateEnqueued -= _handleDelegateEnqueued;
+ }
+
+ // Dispose and destroy the sdl instance
+ if (SdlHandle != nint.Zero) Sdl?.Api.DestroyWindow((Window*) SdlHandle);
+ Sdl?.Dispose();
+ }
+
+ // Dispose unmanaged state (unmanaged objects)
+ // ...
+
+ // Indicate disposal completion
+ _disposed = true;
+ } finally {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml
index 35659c6..e08cdfe 100644
--- a/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml
+++ b/CatalystUI/Tooling/CatalystUI.Profiling/Properties/PublishProfiles/PublishProfiling.pubxml
@@ -16,14 +16,14 @@
ConsoleApp
bin\Release\net9.0\_publish\
-
- win-x64
-
-
- osx-x64
-
-
- linux-x64
+
+ <_Arch>$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString())
+ win-arm64
+ win-x64
+ osx-arm64
+ osx-x64
+ linux-arm64
+ linux-x64