Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ nohup.out
###########################
# CatalystUI Negotiations #
###########################
!**/*/Examples/**/PublishProfiles/PublishExamples.pubxml
!**/*.Profiling/
!**/*.Profiling/*.Profiling.csproj
!**/*.Profiling/Program.cs
Expand Down
36 changes: 34 additions & 2 deletions .scripts/Setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ $projectsList = @(
Depends = @("Arcane")
},
@{
Module = "Crystal"
Module = "Crystal.Core"
Projects = @(
@{ Folder = "Modules/Crystal"; Name = "CatalystUI.Modules.Crystal.Core" }
)
Expand All @@ -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" )
}
)

Expand Down
30 changes: 30 additions & 0 deletions CatalystUI/CatalystUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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
2 changes: 2 additions & 0 deletions CatalystUI/CatalystUI.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ο»Ώ<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GL/@EntryIndexedValue">GL</s:String></wpf:ResourceDictionary>
11 changes: 8 additions & 3 deletions CatalystUI/Core/CatalystUI.Core/CatalystApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Catalyst.Debugging;
using Catalyst.Threading;
using System;
using System.Runtime.ExceptionServices;
using System.Threading;

namespace Catalyst {
Expand Down Expand Up @@ -67,6 +68,7 @@ internal CatalystApp(Func<CatalystApp, int>? 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;
Expand All @@ -77,13 +79,16 @@ internal CatalystApp(Func<CatalystApp, int>? 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;
Expand Down
94 changes: 59 additions & 35 deletions CatalystUI/Core/CatalystUI.Threading/DelegateQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,35 @@ public sealed class DelegateQueue : IDisposable {
public delegate void DelegateQueueEventHandler(DelegateQueue queue, EnqueuedDelegate @delegate);

/// <summary>
/// Invoked when a delegate is enqueued but prior to its execution.
/// An event handler delegate for exception events related to the <see cref="DelegateQueue"/>.
/// </summary>
public delegate void DelegateQueueExceptionEventHandler(DelegateQueue queue, EnqueuedDelegate @delegate, ExceptionDispatchInfo? exception);

/// <summary>
/// Invoked when a delegate is enqueued.
/// </summary>
/// <remarks>
/// Invoked on the calling thread, not the executing thread.
/// <br/><br/>
/// During invocation, the queue lock is held, so avoid long-running
/// operations or deadlocks may occur.
/// </remarks>
public event DelegateQueueEventHandler? DelegateEnqueued;

/// <summary>
/// Invoked when a delegate is dequeued for execution but prior to its execution.
/// Invoked when a delegate is dequeued prior to execution.
/// </summary>
/// <remarks>
/// Invoked on the executing thread, not the calling thread.
/// <br/><br/>
/// During invocation, the queue lock is held, so avoid long-running
/// operations or deadlocks may occur.
/// </remarks>
public event DelegateQueueEventHandler? DelegateDequeued;

/// <summary>
/// Invoked after a delegate has been executed.
/// Invoked when a delegate has been executed successfully.
/// </summary>
/// <remarks>
/// Invoked on the executing thread, not the calling thread.
/// <br/><br/>
/// During invocation, the queue lock is held, so avoid long-running
/// operations or deadlocks may occur.
/// </remarks>
public event DelegateQueueEventHandler? DelegateExecuted;

/// <summary>
/// Invoked when a delegate throws an exception during execution.
/// </summary>
public event DelegateQueueExceptionEventHandler? DelegateException;

/// <summary>
/// Invoked when a delegate has finished execution, regardless of success or failure.
/// </summary>
public event DelegateQueueEventHandler? DelegateFinished;

/// <summary>
/// The queue which is used to store enqueued delegates.
/// </summary>
Expand All @@ -76,6 +73,11 @@ public sealed class DelegateQueue : IDisposable {
/// </summary>
private readonly ManualResetEvent _queueFullWaitHandle;

/// <summary>
/// A wait handle that is signaled when a delegate is enqueued.
/// </summary>
private readonly ManualResetEvent _delegateEnqueuedWaitHandle;

/// <summary>
/// The ID of the thread currently executing the queue.
/// </summary>
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -202,10 +206,16 @@ public bool Enqueue(Delegate @delegate, nint caller, nint parameters, out nint @
/// Loops through the queue and executes all enqueued delegates.
/// </summary>
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
Expand All @@ -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) {
Expand Down Expand Up @@ -252,6 +268,16 @@ private void OnDelegateExecuted(EnqueuedDelegate @delegate) {
DelegateExecuted?.Invoke(this, @delegate);
}

/// <inheritdoc cref="DelegateException"/>
private void OnDelegateException(EnqueuedDelegate @delegate, ExceptionDispatchInfo? exception) {
DelegateException?.Invoke(this, @delegate, exception);
}

/// <inheritdoc cref="DelegateFinished"/>
private void OnDelegateFinished(EnqueuedDelegate @delegate) {
DelegateFinished?.Invoke(this, @delegate);
}

/// <summary>
/// Disposes of the <see cref="DelegateQueue"/>.
/// </summary>
Expand All @@ -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++) {
Expand All @@ -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();
}
Expand Down Expand Up @@ -325,7 +353,7 @@ public record struct EnqueuedDelegate {
/// Gets or sets an exception that occurred during execution of the delegate, if any.
/// </summary>
/// <value>The exception that occurred during execution, or <see langword="null"/> if no exception occurred.</value>
public required Exception? Exception { get; set; }
public required ExceptionDispatchInfo? Exception { get; set; }

/// <summary>
/// Gets or sets a wait handle that can be used to wait for the delegate to complete execution.
Expand Down Expand Up @@ -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;
}
}

Expand Down
Loading