diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f8a05a1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,77 @@
+# ------------------------------------------------------------------------------
+#
+#
+# This code was generated.
+#
+# - To turn off auto-generation set:
+#
+# [GitHubActions (AutoGenerate = false)]
+#
+# - To trigger manual generation invoke:
+#
+# nuke --generate-configuration GitHubActions_ci --host GitHubActions
+#
+#
+# ------------------------------------------------------------------------------
+
+name: ci
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ - 'release/**'
+ pull_request:
+ branches:
+ - 'release/**'
+
+jobs:
+ ubuntu-latest:
+ name: ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+ uses: actions/cache@v3
+ with:
+ path: |
+ .nuke/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Run: Compile, Test'
+ run: ./build.cmd Compile Test
+ macos-latest:
+ name: macos-latest
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+ uses: actions/cache@v3
+ with:
+ path: |
+ .nuke/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Run: Compile, Test'
+ run: ./build.cmd Compile Test
+ windows-latest:
+ name: windows-latest
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+ uses: actions/cache@v3
+ with:
+ path: |
+ .nuke/temp
+ ~/.nuget/packages
+ key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
+ - name: 'Run: Compile, Test'
+ run: ./build.cmd Compile Test
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6ef56e1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Rikarin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 03a50f4..9ccec15 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ Create game engine with requirements:
## Features
- Entity Component System (Arch)
+- Serialization & InputSystem is based on the Stride Engine
## Used libraries
diff --git a/Rin.BuildEngine.Common/BuildStep.cs b/Rin.BuildEngine.Common/BuildStep.cs
index cab7537..3318a59 100644
--- a/Rin.BuildEngine.Common/BuildStep.cs
+++ b/Rin.BuildEngine.Common/BuildStep.cs
@@ -195,12 +195,12 @@ internal void RegisterResult(IExecuteContext executeContext, ResultStatus status
if (StepProcessed != null) {
try {
var outputObjectsGroups = executeContext.GetOutputObjectsGroups();
- MicrothreadLocalDatabases.MountDatabase(outputObjectsGroups);
- StepProcessed(this, new BuildStepEventArgs(this, executeContext.Logger));
+ MicroThreadLocalDatabases.MountDatabase(outputObjectsGroups);
+ StepProcessed(this, new(this, executeContext.Logger));
} catch (Exception ex) {
executeContext.Logger.Error(ex, "Exception in command {Command}", this);
} finally {
- MicrothreadLocalDatabases.UnmountDatabase();
+ MicroThreadLocalDatabases.UnmountDatabase();
}
}
}
diff --git a/Rin.BuildEngine.Common/Command.cs b/Rin.BuildEngine.Common/Command.cs
index 193a9cc..9e9d50e 100644
--- a/Rin.BuildEngine.Common/Command.cs
+++ b/Rin.BuildEngine.Common/Command.cs
@@ -1,5 +1,6 @@
using Rin.Core.Serialization;
using Rin.Core.Serialization.Binary;
+using Rin.Core.Serialization.Storage;
using Rin.Core.Storage;
using Rin.Core.TODO;
using System.Reflection;
diff --git a/Rin.BuildEngine.Common/CommandBuildStep.cs b/Rin.BuildEngine.Common/CommandBuildStep.cs
index eb863f7..960a854 100644
--- a/Rin.BuildEngine.Common/CommandBuildStep.cs
+++ b/Rin.BuildEngine.Common/CommandBuildStep.cs
@@ -1,3 +1,4 @@
+using Rin.Core.IO;
using Rin.Core.Storage;
using Rin.Core.TODO;
@@ -104,7 +105,7 @@ public override async Task Execute(IExecuteContext executeContext,
if (stepInProgress != null) {
Monitor.Exit(executeContext);
monitorExited = true;
- executeContext.Logger.Debug($"Command {Command} delayed because it is currently running...");
+ executeContext.Logger.Debug("Command {Command} delayed because it is currently running...", Command);
status = (await stepInProgress.ExecutedAsync()).Status;
matchingResult = stepInProgress.Result;
} else {
@@ -112,7 +113,7 @@ public override async Task Execute(IExecuteContext executeContext,
Monitor.Exit(executeContext);
monitorExited = true;
- executeContext.Logger.Debug($"Command {Command} scheduled...");
+ executeContext.Logger.Debug("Command {Command} scheduled...", Command);
// Register the cancel callback
var cancellationTokenSource = executeContext.CancellationTokenSource;
@@ -206,7 +207,7 @@ BuilderContext builderContext
// Merge results from prerequisites
// TODO: This will prevent us from overwriting this asset with different content as it will result in a write conflict
// At some point we _might_ want to get rid of WaitBuildStep/ListBuildStep system and write a fully stateless input/output-based system; probably need further discussions
- var fileProvider = MicrothreadLocalDatabases.DatabaseFileProvider;
+ var fileProvider = MicroThreadLocalDatabases.DatabaseFileProvider;
if (fileProvider != null) {
var assetIndexMap = fileProvider.ContentIndexMap;
foreach (var prerequisiteStep in PrerequisiteSteps) {
@@ -256,11 +257,11 @@ internal bool ShouldExecute(
out CommandResultEntry matchingResult
) {
var outputObjectsGroups = executeContext.GetOutputObjectsGroups();
- MicrothreadLocalDatabases.MountDatabase(outputObjectsGroups);
+ MicroThreadLocalDatabases.MountDatabase(outputObjectsGroups);
try {
matchingResult = FindMatchingResult(executeContext, previousResultCollection);
} finally {
- MicrothreadLocalDatabases.UnmountDatabase();
+ MicroThreadLocalDatabases.UnmountDatabase();
}
if (matchingResult == null || Command.ShouldForceExecution()) {
diff --git a/Rin.BuildEngine.Common/FileVersionStorage.cs b/Rin.BuildEngine.Common/FileVersionStorage.cs
index d78e7ea..ac77789 100644
--- a/Rin.BuildEngine.Common/FileVersionStorage.cs
+++ b/Rin.BuildEngine.Common/FileVersionStorage.cs
@@ -1,3 +1,5 @@
+using Rin.Core.Serialization;
+using Rin.Core.Serialization.Serializers;
using Rin.Core.Storage;
using System.Text;
@@ -35,8 +37,7 @@ public static bool Compact(string storagePath) {
new Dictionary>(StringComparer.OrdinalIgnoreCase);
foreach (var keyValue in localTracker.GetValues()) {
var filePath = keyValue.Key.Path;
- KeyValuePair previousKeyValue;
- if (!latestVersion.TryGetValue(filePath, out previousKeyValue)
+ if (!latestVersion.TryGetValue(filePath, out var previousKeyValue)
|| keyValue.Key.LastModifiedDate > previousKeyValue.Key.LastModifiedDate) {
latestVersion[filePath] = keyValue;
}
@@ -68,21 +69,21 @@ protected override List> ReadEntries(Stre
var key = new FileVersionKey { Path = values[0] };
if (!long.TryParse(values[1], out var dateTime)) {
throw new InvalidOperationException(
- "Unable to decode datetime [{0}] when reading file version index".ToFormat(values[1])
+ $"Unable to decode datetime [{values[1]}] when reading file version index"
);
}
key.LastModifiedDate = new(dateTime);
if (!long.TryParse(values[2], out key.FileSize)) {
throw new InvalidOperationException(
- "Unable to decode filesize [{0}] when reading file version index".ToFormat(values[2])
+ $"Unable to decode filesize [{values[2]}] when reading file version index"
);
}
var objectIdStr = values[3];
if (!ObjectId.TryParse(objectIdStr, out ObjectId objectId)) {
throw new InvalidOperationException(
- "Unable to decode objectid [{0}] when reading file version index".ToFormat(objectIdStr)
+ $"Unable to decode ObjectId [{objectIdStr}] when reading file version index"
);
}
diff --git a/Rin.BuildEngine.Common/FileVersionTracker.cs b/Rin.BuildEngine.Common/FileVersionTracker.cs
index dd3ee43..8dabe89 100644
--- a/Rin.BuildEngine.Common/FileVersionTracker.cs
+++ b/Rin.BuildEngine.Common/FileVersionTracker.cs
@@ -1,3 +1,4 @@
+using Rin.Core.Storage;
using Serilog;
namespace Rin.BuildEngine.Common;
diff --git a/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs b/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs
new file mode 100644
index 0000000..e7bbc34
--- /dev/null
+++ b/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs
@@ -0,0 +1,109 @@
+using Rin.Core.TODO;
+
+namespace Rin.BuildEngine.Common;
+
+///
+/// A static class that allows to have a different on each .
+/// Objects can still be shared
+/// between micro-threads by using the method.
+///
+public static class MicroThreadLocalDatabases {
+ static readonly Dictionary SharedOutputObjects = new();
+ static readonly MicroThreadLocal MicroThreadLocalDatabaseFileProvider;
+
+ public static IDatabaseFileProviderService ProviderService { get; }
+
+ ///
+ /// Gets a value indicating whether this instance has a valid database file provider.
+ ///
+ /// true if this instance has database file provider; otherwise, false.
+ public static bool HasValidDatabaseFileProvider => MicroThreadLocalDatabaseFileProvider.Value != null;
+
+ ///
+ /// Gets the currently mounted microthread-local database provider.
+ ///
+ public static DatabaseFileProvider DatabaseFileProvider => MicroThreadLocalDatabaseFileProvider.Value;
+
+ static MicroThreadLocalDatabases() {
+ MicroThreadLocalDatabaseFileProvider = new MicroThreadLocal();
+
+ ProviderService = new MicroThreadLocalProviderService();
+ }
+
+ ///
+ /// Merges the given dictionary of build output objects into the shared group. Objects merged here will be integrated
+ /// to every database.
+ ///
+ /// The dictionary containing the to merge into the shared group.
+ public static void AddToSharedGroup(IReadOnlyDictionary outputObjects) {
+ lock (SharedOutputObjects) {
+ foreach (var outputObject in outputObjects) {
+ SharedOutputObjects[outputObject.Key] = outputObject.Value;
+ }
+ }
+ }
+
+ ///
+ /// Gets a containing only objects from the shared group.
+ /// The shared group is a group of objects registered via and shared amongst all
+ /// databases.
+ ///
+ /// A that can provide objects from the common group.
+ public static DatabaseFileProvider GetSharedDatabase() => CreateDatabase(CreateTransaction(null));
+
+ ///
+ /// Creates and mounts a database containing the given output object groups and the shared group in the
+ /// microthread-local
+ /// .
+ ///
+ /// A collection of dictionaries representing a group of output object.
+ public static void MountDatabase(IEnumerable> outputObjectsGroups) {
+ MountDatabase(CreateTransaction(outputObjectsGroups));
+ }
+
+ ///
+ /// Creates and mounts a database containing output objects from the shared group in the microthread-local
+ /// .
+ ///
+ public static void MountCommonDatabase() {
+ MicroThreadLocalDatabaseFileProvider.Value = CreateDatabase(CreateTransaction(null));
+ }
+
+ ///
+ /// Unmounts the currently mounted microthread-local database.
+ ///
+ public static void UnmountDatabase() {
+ MicroThreadLocalDatabaseFileProvider.ClearValue();
+ }
+
+ public static IEnumerable> GetOutputObjectsGroups(
+ IEnumerable> transactionOutputObjectsGroups
+ ) {
+ if (transactionOutputObjectsGroups != null) {
+ foreach (var outputObjects in transactionOutputObjectsGroups) {
+ yield return outputObjects;
+ }
+ }
+
+ yield return SharedOutputObjects;
+ }
+
+ static DatabaseFileProvider CreateDatabase(BuildTransaction transaction) =>
+ new DatabaseFileProvider(new BuildTransaction.DatabaseContentIndexMap(transaction), Builder.ObjectDatabase);
+
+ internal static void MountDatabase(BuildTransaction transaction) {
+ MicroThreadLocalDatabaseFileProvider.Value = CreateDatabase(transaction);
+ }
+
+ internal static BuildTransaction CreateTransaction(
+ IEnumerable> transactionOutputObjectsGroups
+ ) =>
+ new BuildTransaction(
+ Builder.ObjectDatabase.ContentIndexMap,
+ GetOutputObjectsGroups(transactionOutputObjectsGroups)
+ );
+
+ class MicroThreadLocalProviderService : IDatabaseFileProviderService {
+ public DatabaseFileProvider FileProvider => MicroThreadLocalDatabaseFileProvider.Value;
+ }
+}
diff --git a/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj b/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj
index eadbca0..021419a 100644
--- a/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj
+++ b/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj
@@ -5,6 +5,8 @@
enable
+
+
diff --git a/Rin.Core.MicroThreading/Channel.cs b/Rin.Core.MicroThreading/Channel.cs
new file mode 100644
index 0000000..593e2be
--- /dev/null
+++ b/Rin.Core.MicroThreading/Channel.cs
@@ -0,0 +1,107 @@
+namespace Rin.Core.MicroThreading;
+
+///
+/// Provides a communication mechanism between .
+///
+///
+/// can send and receive to a . Depending on the
+/// ,
+/// sending or receiving might be suspended and yield execution to another
+/// .
+///
+/// The type of element handled by this channel.
+// TODO: Thread-safety
+public class Channel {
+ readonly Queue> receivers = new();
+ readonly Queue> senders = new();
+
+ ///
+ /// Gets or sets the preference, allowing you to customize how and behave
+ /// regarding scheduling.
+ ///
+ ///
+ /// The preference.
+ ///
+ public ChannelPreference Preference { get; set; }
+
+ ///
+ /// Gets the balance, which is the number of waiting to send (if greater than 0) or receive
+ /// (if smaller than 0).
+ ///
+ ///
+ /// The balance.
+ ///
+ public int Balance => senders.Count - receivers.Count;
+
+ public Channel() {
+ Preference = ChannelPreference.PreferReceiver;
+ }
+
+ public void Reset() {
+ receivers.Clear();
+ senders.Clear();
+ }
+
+ ///
+ /// Sends a value over the channel. If no other is waiting for data, the sender will be
+ /// blocked.
+ /// If someone was waiting for data, which of the sender or receiver continues next depends on
+ /// .
+ ///
+ /// The data.
+ /// Awaitable data.
+ public ChannelMicroThreadAwaiter Send(T data) {
+ if (receivers.Count == 0) {
+ // Nobody receiving, let's wait until something comes up
+ var microThread = MicroThread.Current;
+ var waitingMicroThread = ChannelMicroThreadAwaiter.New(microThread);
+ waitingMicroThread.Result = data;
+ senders.Enqueue(waitingMicroThread);
+ return waitingMicroThread;
+ }
+
+ var receiver = receivers.Dequeue();
+ receiver.Result = data;
+ if (Preference == ChannelPreference.PreferSender) {
+ receiver.MicroThread.ScheduleContinuation(ScheduleMode.Last, receiver.Continuation);
+ } else if (Preference == ChannelPreference.PreferReceiver) {
+ receiver.MicroThread.ScheduleContinuation(ScheduleMode.First, receiver.Continuation);
+ throw new NotImplementedException();
+ //await Scheduler.Yield();
+ }
+
+ receiver.IsCompleted = true;
+ return receiver;
+ }
+
+ ///
+ /// Receives a value over the channel. If no other is sending data, the receiver will be
+ /// blocked.
+ /// If someone was sending data, which of the sender or receiver continues next depends on .
+ ///
+ /// Awaitable data.
+ public ChannelMicroThreadAwaiter Receive() {
+ if (senders.Count == 0) {
+ var microThread = MicroThread.Current;
+ if (microThread == null) {
+ throw new("Cannot receive out of micro-thread context.");
+ }
+
+ var waitingMicroThread = ChannelMicroThreadAwaiter.New(microThread);
+ receivers.Enqueue(waitingMicroThread);
+ return waitingMicroThread;
+ }
+
+ var sender = senders.Dequeue();
+ if (Preference == ChannelPreference.PreferReceiver) {
+ sender.MicroThread.ScheduleContinuation(ScheduleMode.Last, sender.Continuation);
+ } else if (Preference == ChannelPreference.PreferSender) {
+ sender.MicroThread.ScheduleContinuation(ScheduleMode.First, sender.Continuation);
+ throw new NotImplementedException();
+ //await Scheduler.Yield();
+ }
+
+ sender.IsCompleted = true;
+ return sender;
+ }
+}
diff --git a/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs b/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs
new file mode 100644
index 0000000..36f8f56
--- /dev/null
+++ b/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs
@@ -0,0 +1,68 @@
+using System.Runtime.CompilerServices;
+
+namespace Rin.Core.MicroThreading;
+
+public class ChannelMicroThreadAwaiter : ICriticalNotifyCompletion {
+ internal MicroThread? MicroThread;
+ internal Action Continuation;
+ internal T Result;
+ static readonly List> pool = new();
+
+ bool isCompleted;
+
+ public bool IsCompleted {
+ get => isCompleted || MicroThread is { IsOver: true };
+ set => isCompleted = value;
+ }
+
+ public ChannelMicroThreadAwaiter(MicroThread microThread) {
+ MicroThread = microThread;
+ }
+
+ public static ChannelMicroThreadAwaiter New(MicroThread microThread) {
+ lock (pool) {
+ if (pool.Count > 0) {
+ var index = pool.Count - 1;
+ var lastItem = pool[index];
+ pool.RemoveAt(index);
+
+ lastItem.MicroThread = microThread;
+ return lastItem;
+ }
+
+ return new(microThread);
+ }
+ }
+
+ public ChannelMicroThreadAwaiter GetAwaiter() => this;
+
+ public void OnCompleted(Action continuation) {
+ Continuation = continuation;
+ }
+
+ public void UnsafeOnCompleted(Action continuation) {
+ Continuation = continuation;
+ }
+
+ public T GetResult() {
+ // Check Task Result (exception, etc...)
+ MicroThread.CancellationToken.ThrowIfCancellationRequested();
+
+ var result = Result;
+
+ // After result has been taken, we can reuse this item, so put it in the pool
+ // We mitigate pool size, but another approach than hard limit might be interesting
+ lock (pool) {
+ if (pool.Count < 4096) {
+ isCompleted = false;
+ MicroThread = null;
+ Continuation = null;
+ Result = default;
+ }
+
+ pool.Add(this);
+ }
+
+ return result;
+ }
+}
diff --git a/Rin.Core.MicroThreading/ChannelPreference.cs b/Rin.Core.MicroThreading/ChannelPreference.cs
new file mode 100644
index 0000000..3998a89
--- /dev/null
+++ b/Rin.Core.MicroThreading/ChannelPreference.cs
@@ -0,0 +1,7 @@
+namespace Rin.Core.MicroThreading;
+
+public enum ChannelPreference {
+ PreferReceiver = -1,
+ //Neutral = 0,
+ PreferSender = 1
+}
diff --git a/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs b/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs
new file mode 100644
index 0000000..62f5957
--- /dev/null
+++ b/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs
@@ -0,0 +1,5 @@
+namespace Rin.Core.MicroThreading;
+
+interface IMicroThreadSynchronizationContext {
+ MicroThread MicroThread { get; }
+}
diff --git a/Rin.Core.MicroThreading/MicroThread.cs b/Rin.Core.MicroThreading/MicroThread.cs
new file mode 100644
index 0000000..467c514
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThread.cs
@@ -0,0 +1,283 @@
+using Rin.Core.Collections;
+using Rin.Diagnostics;
+using System.Diagnostics;
+
+namespace Rin.Core.MicroThreading;
+
+///
+/// Represents an execution context managed by a , that can cooperatively yield execution to
+/// another at any point (usually using async calls).
+///
+public class MicroThread {
+ ///
+ /// Gets the attached properties to this component.
+ ///
+ public PropertyContainer Tags;
+
+ internal ProfilingKey? ProfilingKey;
+ internal PriorityQueueNode ScheduledLinkedListNode;
+ internal LinkedListNode AllLinkedListNode; // Also used as lock for "CompletionTask"
+ internal MicroThreadCallbackList Callbacks;
+ internal SynchronizationContext SynchronizationContext;
+
+ static long globalCounterId;
+
+ int state;
+ readonly CancellationTokenSource cancellationTokenSource;
+
+ ///
+ /// Gets or sets the priority of this .
+ ///
+ ///
+ /// The priority.
+ ///
+ public long Priority {
+ get => ScheduledLinkedListNode.Value.Priority;
+ set {
+ if (ScheduledLinkedListNode.Value.Priority != value) {
+ Reschedule(ScheduleMode.First, value);
+ }
+ }
+ }
+
+ ///
+ /// Gets the id of this .
+ ///
+ ///
+ /// The id.
+ ///
+ public long Id { get; private set; }
+
+ ///
+ /// Gets or sets the name of this .
+ ///
+ ///
+ /// The name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets the scheduler associated with this .
+ ///
+ /// The scheduler associated with this .
+ public Scheduler Scheduler { get; private set; }
+
+ ///
+ /// Gets the state of this .
+ ///
+ /// The state of this .
+ public MicroThreadState State {
+ get => (MicroThreadState)state;
+ internal set => state = (int)value;
+ }
+
+ ///
+ /// Gets the exception that was thrown by this .
+ ///
+ /// It could come from either internally, or from
+ ///
+ /// if it was successfully processed.
+ /// The exception.
+ public Exception? Exception { get; private set; }
+
+ ///
+ /// Gets the flags.
+ ///
+ ///
+ /// The flags.
+ ///
+ public MicroThreadFlags Flags { get; private set; }
+
+ ///
+ /// Gets or sets the scheduling mode.
+ ///
+ ///
+ /// The scheduling mode.
+ ///
+ public ScheduleMode ScheduleMode { get; set; }
+
+ ///
+ /// A token for listening to the cancellation of the MicroThread.
+ ///
+ public CancellationToken CancellationToken => cancellationTokenSource.Token;
+
+ ///
+ /// Indicates whether the MicroThread is terminated or not, either in Completed, Canceled or Failed status.
+ ///
+ public bool IsOver => State is MicroThreadState.Completed or MicroThreadState.Canceled or MicroThreadState.Failed;
+
+ ///
+ /// Gets the current micro thread (self).
+ ///
+ /// The current micro thread (self).
+ public static MicroThread Current => Scheduler.CurrentMicroThread;
+
+ ///
+ /// Gets or sets the task that will be executed upon completion (used internally for )
+ ///
+ internal TaskCompletionSource? CompletionTask { get; set; }
+
+ public MicroThread(Scheduler scheduler, MicroThreadFlags flags = MicroThreadFlags.None) {
+ Id = Interlocked.Increment(ref globalCounterId);
+ Scheduler = scheduler;
+ ScheduledLinkedListNode = new(new(this));
+ AllLinkedListNode = new(this);
+ ScheduleMode = ScheduleMode.Last;
+ Flags = flags;
+ Tags = new(this);
+ cancellationTokenSource = new();
+ }
+
+ public void Migrate(Scheduler scheduler) {
+ throw new NotImplementedException();
+ }
+
+ public void Remove() {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Starts this with the specified function.
+ ///
+ /// The micro thread function.
+ /// The schedule mode.
+ /// MicroThread was already started before.
+ public void Start(Func microThreadFunction, ScheduleMode scheduleMode = ScheduleMode.Last) {
+ // TODO: Interlocked compare exchange?
+ if (Interlocked.CompareExchange(ref state, (int)MicroThreadState.Starting, (int)MicroThreadState.None)
+ != (int)MicroThreadState.None) {
+ throw new InvalidOperationException("MicroThread was already started before.");
+ }
+
+ async void WrappedMicroThreadFunction() {
+ try {
+ State = MicroThreadState.Running;
+
+ await microThreadFunction();
+
+ if (State != MicroThreadState.Running) {
+ throw new InvalidOperationException("MicroThread completed in an invalid state.");
+ }
+
+ State = MicroThreadState.Completed;
+ } catch (OperationCanceledException e) {
+ // Exit gracefully on cancellation exceptions
+ SetException(e);
+ } catch (Exception e) {
+ Scheduler.Log.Error("Unexpected exception while executing a micro-thread", e);
+ SetException(e);
+ } finally {
+ lock (Scheduler.AllMicroThreads) {
+ Scheduler.AllMicroThreads.Remove(AllLinkedListNode);
+ }
+ }
+ }
+
+ void Callback() {
+ SynchronizationContext = new MicroThreadSynchronizationContext(this);
+ SynchronizationContext.SetSynchronizationContext(SynchronizationContext);
+
+ WrappedMicroThreadFunction();
+ }
+
+ lock (Scheduler.AllMicroThreads) {
+ Scheduler.AllMicroThreads.AddLast(AllLinkedListNode);
+ }
+
+ ScheduleContinuation(scheduleMode, Callback);
+ }
+
+ ///
+ /// Yields to this .
+ ///
+ /// Task.
+ public async Task Run() {
+ Reschedule(ScheduleMode.First, Priority);
+ var currentScheduler = Scheduler.Current;
+ if (currentScheduler == Scheduler) {
+ await Scheduler.Yield();
+ }
+ }
+
+ ///
+ /// Cancels the .
+ ///
+ public void Cancel() {
+ // TODO: If we unschedule the microthread after cancellation, we never give user code the chance to throw OperationCanceledException.
+ // If we don't, we can't be sure that the MicroThread ends.
+ // Should we run continuations manually?
+
+ // Notify awaitables
+ cancellationTokenSource.Cancel();
+
+ // Unschedule microthread
+ //lock (Scheduler.scheduledMicroThreads)
+ //{
+ // if (ScheduledLinkedListNode.Index != -1)
+ // {
+ // Scheduler.scheduledMicroThreads.Remove(ScheduledLinkedListNode);
+ // }
+ //}
+ }
+
+ MicroThreadCallbackNode NewCallback() {
+ MicroThreadCallbackNode node;
+ var pool = Scheduler.CallbackNodePool;
+
+ if (Scheduler.CallbackNodePool.Count > 0) {
+ var index = pool.Count - 1;
+ node = pool[index];
+ pool.RemoveAt(index);
+ } else {
+ node = new();
+ }
+
+ return node;
+ }
+
+ internal void SetException(Exception exception) {
+ Exception = exception;
+
+ // Depending on if exception was raised from outside or inside, set appropriate state
+ State = exception is OperationCanceledException ? MicroThreadState.Canceled : MicroThreadState.Failed;
+ }
+
+ internal void Reschedule(ScheduleMode scheduleMode, long newPriority) {
+ lock (Scheduler.ScheduledEntries) {
+ if (ScheduledLinkedListNode.Index != -1) {
+ Scheduler.ScheduledEntries.Remove(ScheduledLinkedListNode);
+ ScheduledLinkedListNode.Value.Priority = newPriority;
+ Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode);
+ } else {
+ ScheduledLinkedListNode.Value.Priority = newPriority;
+ }
+ }
+ }
+
+ internal void ScheduleContinuation(ScheduleMode scheduleMode, SendOrPostCallback callback, object callbackState) {
+ Debug.Assert(callback != null);
+ lock (Scheduler.ScheduledEntries) {
+ var node = NewCallback();
+ node.SendOrPostCallback = callback;
+ node.CallbackState = callbackState;
+ Callbacks.Add(node);
+
+ if (ScheduledLinkedListNode.Index == -1) {
+ Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode);
+ }
+ }
+ }
+
+ internal void ScheduleContinuation(ScheduleMode scheduleMode, Action callback) {
+ Debug.Assert(callback != null);
+ lock (Scheduler.ScheduledEntries) {
+ var node = NewCallback();
+ node.MicroThreadAction = callback;
+ Callbacks.Add(node);
+
+ if (ScheduledLinkedListNode.Index == -1) {
+ Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode);
+ }
+ }
+ }
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadCallbackList.cs b/Rin.Core.MicroThreading/MicroThreadCallbackList.cs
new file mode 100644
index 0000000..bf1fc21
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadCallbackList.cs
@@ -0,0 +1,29 @@
+namespace Rin.Core.MicroThreading;
+
+struct MicroThreadCallbackList {
+ public MicroThreadCallbackNode First { get; private set; }
+ public MicroThreadCallbackNode Last { get; private set; }
+
+ public void Add(MicroThreadCallbackNode node) {
+ if (First == null) {
+ First = node;
+ } else {
+ Last.Next = node;
+ }
+
+ Last = node;
+ }
+
+ public bool TakeFirst(out MicroThreadCallbackNode callback) {
+ callback = First;
+
+ if (First == null) {
+ return false;
+ }
+
+ First = callback.Next;
+ callback.Next = null;
+
+ return true;
+ }
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs b/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs
new file mode 100644
index 0000000..57961ff
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs
@@ -0,0 +1,22 @@
+namespace Rin.Core.MicroThreading;
+
+class MicroThreadCallbackNode {
+ public Action? MicroThreadAction;
+ public SendOrPostCallback? SendOrPostCallback;
+ public object? CallbackState;
+ public MicroThreadCallbackNode? Next;
+
+ public void Invoke() {
+ if (MicroThreadAction != null) {
+ MicroThreadAction();
+ } else {
+ SendOrPostCallback(CallbackState);
+ }
+ }
+
+ public void Clear() {
+ MicroThreadAction = null;
+ SendOrPostCallback = null;
+ CallbackState = null;
+ }
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadFlags.cs b/Rin.Core.MicroThreading/MicroThreadFlags.cs
new file mode 100644
index 0000000..dded586
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadFlags.cs
@@ -0,0 +1,19 @@
+namespace Rin.Core.MicroThreading;
+
+[Flags]
+public enum MicroThreadFlags {
+ None = 0,
+
+ ///
+ /// If a faulted is not being waited on, do not propagate exception outside of
+ /// .
+ ///
+ ///
+ /// If an exception happens in a , two things can happen.
+ /// Either something was waiting on it (i.e. with ), in that case exception will be
+ /// propagated to waiting code.
+ /// Otherwise, exception will be rethrow outside of .
+ /// This flags allows exception to be ignored even if nothing was waiting on it.
+ ///
+ IgnoreExceptions = 1
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadLocal.cs b/Rin.Core.MicroThreading/MicroThreadLocal.cs
new file mode 100644
index 0000000..655fe82
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadLocal.cs
@@ -0,0 +1,103 @@
+using System.Runtime.CompilerServices;
+
+namespace Rin.Core.MicroThreading;
+
+///
+/// Provides MicroThread-local storage of data.
+///
+/// Type of data stored.
+public class MicroThreadLocal where T : class {
+ readonly Func? valueFactory;
+ readonly ConditionalWeakTable values = new();
+
+ ///
+ /// The value return when we are not in a micro thread. That is the value return when
+ /// 'Scheduler.CurrentMicroThread==null'
+ ///
+ T? valueOutOfMicroThread;
+
+ ///
+ /// Indicate if the value out of micro-thread have been set at least once or not.
+ ///
+ bool valueOutOfMicroThreadSet;
+
+ ///
+ /// Gets or sets the value for the current microthread.
+ ///
+ ///
+ /// The value for the current microthread.
+ ///
+ public T Value {
+ get {
+ T value;
+ var microThread = Scheduler.CurrentMicroThread;
+
+ lock (values) {
+ if (microThread == null) {
+ if (!valueOutOfMicroThreadSet) {
+ valueOutOfMicroThread = valueFactory?.Invoke();
+ }
+
+ value = valueOutOfMicroThread;
+ } else if (!values.TryGetValue(microThread, out value)) {
+ values.Add(microThread, value = valueFactory != null ? valueFactory() : default);
+ }
+ }
+
+ return value;
+ }
+ set {
+ var microThread = Scheduler.CurrentMicroThread;
+
+ lock (values) {
+ if (microThread == null) {
+ valueOutOfMicroThread = value;
+ valueOutOfMicroThreadSet = true;
+ } else {
+ values.Remove(microThread);
+ values.Add(microThread, value);
+ }
+ }
+ }
+ }
+
+ public bool IsValueCreated {
+ get {
+ var microThread = Scheduler.CurrentMicroThread;
+
+ lock (values) {
+ return microThread == null ? valueOutOfMicroThreadSet : values.TryGetValue(microThread, out _);
+ }
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MicroThreadLocal()
+ : this(null) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The value factory invoked to create a value when is retrieved before
+ /// having been previously initialized.
+ ///
+ public MicroThreadLocal(Func valueFactory) {
+ this.valueFactory = valueFactory;
+ }
+
+ public void ClearValue() {
+ var microThread = Scheduler.CurrentMicroThread;
+
+ lock (values) {
+ if (microThread == null) {
+ valueOutOfMicroThread = default;
+ valueOutOfMicroThreadSet = false;
+ } else {
+ values.Remove(microThread);
+ }
+ }
+ }
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadProfiling.cs b/Rin.Core.MicroThreading/MicroThreadProfiling.cs
new file mode 100644
index 0000000..3ebf18f
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadProfiling.cs
@@ -0,0 +1,14 @@
+using Rin.Diagnostics;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+
+namespace Rin.Core.MicroThreading;
+
+public static class MicroThreadProfiling {
+ public static readonly ActivitySource MicroThreadingSource = new("Rin.Core.MicroThreading");
+ public static readonly Meter MicroThreadingMeter = new("Rin.Core.MicroThreading");
+
+ public static readonly Histogram MicroThread = MicroThreadingMeter.CreateHistogram("MicroThread");
+
+ public static readonly ProfilingKey ProfilingKey = new(MicroThread, MicroThreadingSource.StartActivity("MicroThread"));
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadState.cs b/Rin.Core.MicroThreading/MicroThreadState.cs
new file mode 100644
index 0000000..b6d2ff7
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadState.cs
@@ -0,0 +1,10 @@
+namespace Rin.Core.MicroThreading;
+
+public enum MicroThreadState {
+ None,
+ Starting,
+ Running,
+ Completed,
+ Canceled,
+ Failed
+}
\ No newline at end of file
diff --git a/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs b/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs
new file mode 100644
index 0000000..2a1b2e6
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs
@@ -0,0 +1,30 @@
+namespace Rin.Core.MicroThreading;
+
+public class MicroThreadSynchronizationContext : SynchronizationContext, IMicroThreadSynchronizationContext {
+ readonly MicroThread microThread;
+
+ MicroThread IMicroThreadSynchronizationContext.MicroThread => microThread;
+
+ public MicroThreadSynchronizationContext(MicroThread microThread) {
+ this.microThread = microThread;
+ }
+
+ public override SynchronizationContext CreateCopy() => this;
+
+ public override void Post(SendOrPostCallback d, object state) {
+ // There is two case:
+ // 1/ We are either in normal MicroThread inside Scheduler.Step() (CurrentThread test),
+ // in which case we will directly execute the callback to avoid further processing from scheduler.
+ // Also, note that Wait() sends us event that are supposed to come back into scheduler.
+ // Note: As it will end up on the callstack, it might be better to Schedule it instead (to avoid overflow)?
+ // 2/ Otherwise, we just received an external task continuation (i.e. Task.Sleep()), or a microthread triggering another,
+ // so schedule it so that it comes back in our regular scheduler.
+ if (microThread.Scheduler.RunningMicroThread == microThread) {
+ d(state);
+ } else if (microThread.State == MicroThreadState.Completed) {
+ throw new InvalidOperationException("MicroThread is already completed but still posting continuations.");
+ } else {
+ microThread.ScheduleContinuation(microThread.ScheduleMode, d, state);
+ }
+ }
+}
diff --git a/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs b/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs
new file mode 100644
index 0000000..95dab42
--- /dev/null
+++ b/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs
@@ -0,0 +1,33 @@
+using System.Runtime.CompilerServices;
+
+namespace Rin.Core.MicroThreading;
+
+public struct MicroThreadYieldAwaiter : INotifyCompletion {
+ readonly MicroThread microThread;
+
+ public bool IsCompleted {
+ get {
+ if (microThread.IsOver) {
+ return true;
+ }
+
+ lock (microThread.Scheduler.ScheduledEntries) {
+ return microThread.Scheduler.ScheduledEntries.Count == 0;
+ }
+ }
+ }
+
+ public MicroThreadYieldAwaiter(MicroThread microThread) {
+ this.microThread = microThread;
+ }
+
+ public MicroThreadYieldAwaiter GetAwaiter() => this;
+
+ public void GetResult() {
+ microThread.CancellationToken.ThrowIfCancellationRequested();
+ }
+
+ public void OnCompleted(Action continuation) {
+ microThread.ScheduleContinuation(ScheduleMode.Last, continuation);
+ }
+}
diff --git a/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj b/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj
index 9d130cd..83ad43a 100644
--- a/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj
+++ b/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj
@@ -4,4 +4,12 @@
enableenable
+
+
+
+
+
+
+
+
diff --git a/Rin.Core.MicroThreading/ScheduleMode.cs b/Rin.Core.MicroThreading/ScheduleMode.cs
new file mode 100644
index 0000000..f779af5
--- /dev/null
+++ b/Rin.Core.MicroThreading/ScheduleMode.cs
@@ -0,0 +1,6 @@
+namespace Rin.Core.MicroThreading;
+
+public enum ScheduleMode {
+ First,
+ Last
+}
diff --git a/Rin.Core.MicroThreading/Scheduler.cs b/Rin.Core.MicroThreading/Scheduler.cs
new file mode 100644
index 0000000..78b818c
--- /dev/null
+++ b/Rin.Core.MicroThreading/Scheduler.cs
@@ -0,0 +1,321 @@
+using Rin.Core.Collections;
+using Rin.Diagnostics;
+using Serilog;
+using System.Runtime.ExceptionServices;
+
+namespace Rin.Core.MicroThreading;
+
+///
+/// Scheduler that manage a group of cooperating .
+///
+///
+/// Microthreading provides a way to execute many small execution contexts who cooperatively yield to each others.
+///
+public class Scheduler {
+ // An ever-increasing counter that will be used to have a "stable" microthread scheduling (first added is first scheduled)
+ internal long SchedulerCounter;
+
+ internal PriorityNodeQueue ScheduledEntries = new();
+ internal LinkedList AllMicroThreads = new();
+
+ internal List CallbackNodePool = new();
+
+ // internal static readonly Logger Log = GlobalLogger.GetLogger("Scheduler");
+ readonly ILogger Log = Serilog.Log.ForContext();
+
+ readonly ThreadLocal runningMicroThread = new();
+
+ ///
+ /// Gets the current running micro thread in this scheduler through .
+ ///
+ /// The current running micro thread in this scheduler.
+ public MicroThread RunningMicroThread => runningMicroThread.Value;
+
+ ///
+ /// Gets the scheduler associated with current micro thread.
+ ///
+ /// The scheduler associated with current micro thread.
+ public static Scheduler? Current => CurrentMicroThread?.Scheduler;
+
+ ///
+ /// Gets the list of every non-stopped micro threads.
+ ///
+ /// The list of every non-stopped micro threads.
+ public ICollection MicroThreads => AllMicroThreads;
+
+ ///
+ /// Gets the current micro thread (self).
+ ///
+ /// The current micro thread (self).
+ public static MicroThread? CurrentMicroThread =>
+ (SynchronizationContext.Current as IMicroThreadSynchronizationContext)?.MicroThread;
+
+ protected Channel FrameChannel { get; }
+
+ ///
+ /// Gets or sets a value indicating whether microthread exceptions are propagated (and crashes the process). Default to
+ /// true.
+ /// This can be overridden to false per by using
+ /// .
+ ///
+ ///
+ /// true if [propagate exceptions]; otherwise, false.
+ ///
+ internal bool PropagateExceptions { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Scheduler() {
+ PropagateExceptions = true;
+ FrameChannel = new() { Preference = ChannelPreference.PreferSender };
+ }
+
+ ///
+ /// Yields execution.
+ /// If any other micro thread is pending, it will be run now and current micro thread will be scheduled as last.
+ ///
+ /// Task that will resume later during same frame.
+ public static MicroThreadYieldAwaiter Yield() => new(CurrentMicroThread);
+
+ ///
+ /// Yields execution until next frame.
+ ///
+ /// Task that will resume next frame.
+ public ChannelMicroThreadAwaiter NextFrame() {
+ if (MicroThread.Current == null) {
+ throw new("NextFrame cannot be called out of the micro-thread context.");
+ }
+
+ return FrameChannel.Receive();
+ }
+
+ ///
+ /// Runs until no runnable tasks left.
+ /// This function is reentrant.
+ ///
+ public void Run() {
+ var managedThreadId = Thread.CurrentThread.ManagedThreadId;
+ var callbacks = default(MicroThreadCallbackList);
+
+ while (true) {
+ SchedulerEntry schedulerEntry;
+ MicroThread? microThread;
+ lock (ScheduledEntries) {
+ // Reclaim callbacks of previous microthread
+ MicroThreadCallbackNode callback;
+ while (callbacks.TakeFirst(out callback)) {
+ callback.Clear();
+ CallbackNodePool.Add(callback);
+ }
+
+ if (ScheduledEntries.Count == 0) {
+ break;
+ }
+
+ schedulerEntry = ScheduledEntries.Dequeue();
+ microThread = schedulerEntry.MicroThread;
+ if (microThread != null) {
+ callbacks = microThread.Callbacks;
+ microThread.Callbacks = default;
+ }
+ }
+
+ // Since it can be reentrant, it should be restored after running the callback.
+ var previousRunningMicrothread = runningMicroThread.Value;
+ if (previousRunningMicrothread != null) {
+ MicroThreadCallbackEnd?.Invoke(
+ this,
+ new(previousRunningMicrothread, managedThreadId)
+ );
+ }
+
+ runningMicroThread.Value = microThread;
+
+ if (microThread != null) {
+ var previousSyncContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(microThread.SynchronizationContext);
+
+ // TODO: Do we still need to try/catch here? Everything should be caught in the continuation wrapper and put into MicroThread.Exception
+ try {
+ if (microThread.State == MicroThreadState.Starting) {
+ MicroThreadStarted?.Invoke(this, new(microThread, managedThreadId));
+ }
+
+ MicroThreadCallbackStart?.Invoke(this, new(microThread, managedThreadId));
+
+ var profilingKey = microThread.ProfilingKey
+ ?? schedulerEntry.ProfilingKey ?? MicroThreadProfiling.ProfilingKey;
+ using (profilingKey.Begin()) {
+ var callback = callbacks.First;
+ while (callback != null) {
+ callback.Invoke();
+ callback = callback.Next;
+ }
+ }
+ } catch (Exception e) {
+ Log.Error("Unexpected exception while executing a micro-thread", e);
+ microThread.SetException(e);
+ } finally {
+ MicroThreadCallbackEnd?.Invoke(this, new(microThread, managedThreadId));
+
+ SynchronizationContext.SetSynchronizationContext(previousSyncContext);
+ if (microThread.IsOver) {
+ lock (microThread.AllLinkedListNode) {
+ if (microThread.CompletionTask != null) {
+ if (microThread.State is MicroThreadState.Failed or MicroThreadState.Canceled) {
+ microThread.CompletionTask.TrySetException(microThread.Exception);
+ } else {
+ microThread.CompletionTask.TrySetResult(1);
+ }
+ } else if (microThread is { State: MicroThreadState.Failed, Exception: not null }) {
+ // Nothing was listening on the micro thread and it crashed
+ // Let's treat it as unhandled exception and propagate it
+ // Use ExceptionDispatchInfo.Capture to not overwrite callstack
+ if (PropagateExceptions
+ && (microThread.Flags & MicroThreadFlags.IgnoreExceptions)
+ != MicroThreadFlags.IgnoreExceptions) {
+ ExceptionDispatchInfo.Capture(microThread.Exception).Throw();
+ }
+ }
+
+ MicroThreadEnded?.Invoke(this, new(microThread, managedThreadId));
+ }
+ }
+
+ runningMicroThread.Value = previousRunningMicrothread;
+ if (previousRunningMicrothread != null) {
+ MicroThreadCallbackStart?.Invoke(
+ this,
+ new(previousRunningMicrothread, managedThreadId)
+ );
+ }
+ }
+ } else {
+ try {
+ var profilingKey = schedulerEntry.ProfilingKey ?? MicroThreadProfiling.ProfilingKey;
+ using (profilingKey.Begin()) {
+ schedulerEntry.Action();
+ }
+ } catch (Exception e) {
+ ActionException?.Invoke(this, schedulerEntry, e);
+ }
+ }
+ }
+
+ while (FrameChannel.Balance < 0) {
+ FrameChannel.Send(0);
+ }
+ }
+
+ ///
+ /// Creates a micro thread out of the specified function and schedules it as last micro thread to run in this
+ /// scheduler.
+ /// Note that in case of multithreaded scheduling, it might start before this function returns.
+ ///
+ /// The function to create a micro thread from.
+ /// The flags.
+ /// A micro thread.
+ public MicroThread Add(Func microThreadFunction, MicroThreadFlags flags = MicroThreadFlags.None) {
+ var microThread = new MicroThread(this, flags);
+ microThread.Start(microThreadFunction);
+ return microThread;
+ }
+
+ ///
+ /// Creates a new empty micro thread, that could later be started with .
+ ///
+ /// A new empty micro thread.
+ public MicroThread Create() => new(this);
+
+ ///
+ /// Task that will completes when all MicroThread executions are completed.
+ ///
+ /// The micro threads.
+ /// A task that will complete when all micro threads are complete.
+ public async Task WhenAll(params MicroThread[] microThreads) {
+ var currentMicroThread = CurrentMicroThread;
+ Task[] continuationTasks;
+ var tcs = new TaskCompletionSource();
+
+ // Need additional checks: Not sure if we should switch to return a Task and set it before returning it.
+ // It should continue execution right away (no execution flow yielding).
+ lock (microThreads) {
+ if (microThreads.All(x => x.State == MicroThreadState.Completed)) {
+ return;
+ }
+
+ if (microThreads.Any(x => x.State == MicroThreadState.Failed || x.State == MicroThreadState.Canceled)) {
+ throw new AggregateException(microThreads.Select(x => x.Exception).Where(x => x != null));
+ }
+
+ var completionTasks = new List>();
+ foreach (var thread in microThreads) {
+ if (!thread.IsOver) {
+ lock (thread.AllLinkedListNode) {
+ if (thread.CompletionTask == null) {
+ thread.CompletionTask = new();
+ }
+ }
+
+ completionTasks.Add(thread.CompletionTask.Task);
+ }
+ }
+
+ continuationTasks = completionTasks.ToArray();
+ }
+
+ // Force tasks exception to be checked and propagated
+ await Task.Factory.ContinueWhenAll(continuationTasks, tasks => Task.WaitAll(tasks));
+ }
+
+ public event EventHandler MicroThreadStarted;
+ public event EventHandler MicroThreadEnded;
+
+ public event EventHandler MicroThreadCallbackStart;
+ public event EventHandler MicroThreadCallbackEnd;
+
+ // This is part of temporary internal API, this should be improved before exposed
+ internal event Action ActionException;
+
+ // TODO: We will need a better API than exposing PriorityQueueNode before we can make this public.
+ internal PriorityQueueNode Add(
+ Action simpleAction,
+ int priority = 0,
+ object? token = null,
+ ProfilingKey? profilingKey = null
+ ) {
+ var schedulerEntryNode = new PriorityQueueNode(
+ new(simpleAction, priority) { Token = token, ProfilingKey = profilingKey }
+ );
+ Schedule(schedulerEntryNode, ScheduleMode.Last);
+ return schedulerEntryNode;
+ }
+
+ internal PriorityQueueNode Create(Action simpleAction, long priority) =>
+ new(new(simpleAction, priority));
+
+ internal void Schedule(PriorityQueueNode schedulerEntry, ScheduleMode scheduleMode) {
+ lock (ScheduledEntries) {
+ var nextCounter = SchedulerCounter++;
+ if (scheduleMode == ScheduleMode.First) {
+ nextCounter = -nextCounter;
+ }
+
+ schedulerEntry.Value.SchedulerCounter = nextCounter;
+
+ ScheduledEntries.Enqueue(schedulerEntry);
+ }
+ }
+
+ internal void Unschedule(PriorityQueueNode schedulerEntry) {
+ lock (ScheduledEntries) {
+ if (schedulerEntry.Index != -1) {
+ ScheduledEntries.Remove(schedulerEntry);
+ }
+ }
+ }
+
+ // TODO: Currently kept as a struct, but maybe a class would make more sense?
+ // Ideally it should be merged with PriorityQueueNode so that we need to allocate only one object?
+}
diff --git a/Rin.Core.MicroThreading/SchedulerEntry.cs b/Rin.Core.MicroThreading/SchedulerEntry.cs
new file mode 100644
index 0000000..4a0092f
--- /dev/null
+++ b/Rin.Core.MicroThreading/SchedulerEntry.cs
@@ -0,0 +1,29 @@
+using Rin.Diagnostics;
+
+namespace Rin.Core.MicroThreading;
+
+///
+/// Either a MicroThread or an action with priority.
+///
+struct SchedulerEntry : IComparable {
+ public readonly Action Action;
+ public readonly MicroThread? MicroThread;
+ public long Priority;
+ public long SchedulerCounter;
+ public object Token;
+ public ProfilingKey? ProfilingKey;
+
+ public SchedulerEntry(MicroThread microThread) : this() {
+ MicroThread = microThread;
+ }
+
+ public SchedulerEntry(Action action, long priority) : this() {
+ Action = action;
+ Priority = priority;
+ }
+
+ public int CompareTo(SchedulerEntry other) {
+ var priorityDiff = Priority.CompareTo(other.Priority);
+ return priorityDiff != 0 ? priorityDiff : SchedulerCounter.CompareTo(other.SchedulerCounter);
+ }
+}
diff --git a/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs b/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs
new file mode 100644
index 0000000..790500f
--- /dev/null
+++ b/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs
@@ -0,0 +1,33 @@
+namespace Rin.Core.MicroThreading;
+
+///
+/// Provides data for the , ,
+/// and events.
+///
+public class SchedulerThreadEventArgs : EventArgs {
+ ///
+ /// Gets the this event concerns.
+ ///
+ ///
+ /// The micro thread.
+ ///
+ public MicroThread MicroThread { get; private set; }
+
+ ///
+ /// Gets the active when this event happened.
+ ///
+ ///
+ /// The managed thread identifier.
+ ///
+ public int ThreadId { get; private set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The micro thread.
+ /// The managed thread identifier.
+ public SchedulerThreadEventArgs(MicroThread microThread, int threadId) {
+ MicroThread = microThread;
+ ThreadId = threadId;
+ }
+}
diff --git a/Rin.Core.Serialization/Rin.Core.Serialization.csproj b/Rin.Core.Serialization/Rin.Core.Serialization.csproj
index 9d130cd..9c75c3d 100644
--- a/Rin.Core.Serialization/Rin.Core.Serialization.csproj
+++ b/Rin.Core.Serialization/Rin.Core.Serialization.csproj
@@ -4,4 +4,7 @@
enableenable
+
+
+
diff --git a/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs b/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs
new file mode 100644
index 0000000..d77a4be
--- /dev/null
+++ b/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs
@@ -0,0 +1,12 @@
+using Rin.Core.Storage;
+
+namespace Rin.Core.Serialization.Serialization.Contents;
+
+public interface IContentIndexMap : IDisposable {
+ ObjectId this[string url] { get; set; }
+
+ bool TryGetValue(string url, out ObjectId objectId);
+ bool Contains(string url);
+ IEnumerable> SearchValues(Func, bool> predicate);
+ IEnumerable> GetMergedIdMap();
+}
diff --git a/Rin.Core.Serialization/Storage/DigestStream.cs b/Rin.Core.Serialization/Storage/DigestStream.cs
new file mode 100644
index 0000000..fa3090b
--- /dev/null
+++ b/Rin.Core.Serialization/Storage/DigestStream.cs
@@ -0,0 +1,33 @@
+using Rin.Core.Storage;
+
+namespace Rin.Core.Serialization.Storage;
+
+public class DigestStream : OdbStreamWriter {
+ ObjectIdBuilder builder;
+
+ public override ObjectId CurrentHash => builder.ComputeHash();
+
+ public DigestStream(Stream stream) : base(stream, null) { }
+
+ internal DigestStream(Stream stream, string temporaryName) : base(stream, temporaryName) { }
+
+ public void Reset() {
+ Position = 0;
+ builder.Reset();
+ }
+
+ public override void WriteByte(byte value) {
+ builder.WriteByte(value);
+ stream.WriteByte(value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count) {
+ builder.Write(buffer, offset, count);
+ stream.Write(buffer, offset, count);
+ }
+
+ public override void Write(ReadOnlySpan buffer) {
+ builder.Write(buffer);
+ stream.Write(buffer);
+ }
+}
diff --git a/Rin.Core.Serialization/Storage/IOdbBackend.cs b/Rin.Core.Serialization/Storage/IOdbBackend.cs
new file mode 100644
index 0000000..47c0d1a
--- /dev/null
+++ b/Rin.Core.Serialization/Storage/IOdbBackend.cs
@@ -0,0 +1,91 @@
+using Rin.Core.IO;
+using Rin.Core.Serialization.Serialization.Contents;
+using Rin.Core.Storage;
+
+namespace Rin.Core.Serialization.Storage;
+
+///
+/// Base class for custom object database backends (ODB).
+///
+public interface IOdbBackend : IDisposable {
+ ///
+ /// Gets the asset index map.
+ ///
+ ///
+ /// The asset index map.
+ ///
+ IContentIndexMap ContentIndexMap { get; }
+
+ ///
+ /// Opens a of the object with the specified .
+ ///
+ /// The .
+ /// The mode.
+ /// The access.
+ /// The process share mode.
+ ///
+ /// A opened from the specified .
+ ///
+ Stream OpenStream(
+ ObjectId objectId,
+ VirtualFileMode mode = VirtualFileMode.Open,
+ VirtualFileAccess access = VirtualFileAccess.Read,
+ VirtualFileShare share = VirtualFileShare.Read
+ );
+
+ ///
+ /// Requests that this backend read an object's length (but not its contents).
+ ///
+ /// The .
+ /// The object size.
+ int GetSize(ObjectId objectId);
+
+ ///
+ /// Writes an object to the backing store.
+ /// The backend may need to compute the object ID and return it to the caller.
+ ///
+ ///
+ /// The if already computed, or if not
+ /// determined yet.
+ ///
+ /// The data stream.
+ /// The data length.
+ ///
+ /// Set to true to force writing the datastream even if a content is already stored with the same
+ /// id. Default is false.
+ ///
+ /// The generated .
+ ObjectId Write(ObjectId objectId, Stream dataStream, int length, bool forceWrite = false);
+
+ ///
+ /// Creates a stream that will be saved to database when closed and/or disposed.
+ ///
+ /// a stream writer that should be passed to in order to be stored in the database
+ OdbStreamWriter CreateStream();
+
+ ///
+ /// Determines weither the object with the specified exists.
+ ///
+ /// The to check existence for.
+ /// true if an object with the specified exists; otherwise, false.
+ bool Exists(ObjectId objectId);
+
+ ///
+ /// Enumerates the object stored in this backend.
+ ///
+ ///
+ IEnumerable EnumerateObjects();
+
+ ///
+ /// Deletes the specified .
+ ///
+ /// The object id.
+ void Delete(ObjectId objectId);
+
+ ///
+ /// Returns the file path corresponding to the given id (in the VFS domain), if appliable.
+ ///
+ /// The .
+ /// The file path.
+ string GetFilePath(ObjectId objectId);
+}
diff --git a/Rin.Core.Serialization/Storage/OdbStreamWriter.cs b/Rin.Core.Serialization/Storage/OdbStreamWriter.cs
new file mode 100644
index 0000000..af54044
--- /dev/null
+++ b/Rin.Core.Serialization/Storage/OdbStreamWriter.cs
@@ -0,0 +1,41 @@
+using Rin.Core.Storage;
+
+namespace Rin.Core.Serialization.Storage;
+
+public abstract class OdbStreamWriter : Stream {
+ public Action Disposed;
+
+ public string? TemporaryName;
+ protected readonly Stream stream;
+ readonly long initialPosition;
+
+ public abstract ObjectId CurrentHash { get; }
+ public override bool CanRead => false;
+ public override bool CanSeek => true;
+ public override bool CanWrite => stream.CanWrite;
+ public override long Length => stream.Length - initialPosition;
+
+ public override long Position {
+ get => stream.Position - initialPosition;
+ set => stream.Position = initialPosition + value;
+ }
+
+ protected OdbStreamWriter(Stream stream, string? temporaryName) {
+ this.stream = stream;
+ initialPosition = stream.Position;
+ TemporaryName = temporaryName;
+ }
+
+ public override void Flush() => stream.Flush();
+ public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException();
+ public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
+ public override void SetLength(long value) => throw new InvalidOperationException();
+
+ protected override void Dispose(bool disposing) {
+ // Force hash computation before stream is closed.
+ var hash = CurrentHash;
+ stream.Dispose();
+
+ Disposed?.Invoke(this);
+ }
+}
diff --git a/Rin.Core/Collections/PriorityNodeQueue.cs b/Rin.Core/Collections/PriorityNodeQueue.cs
new file mode 100644
index 0000000..fd7363d
--- /dev/null
+++ b/Rin.Core/Collections/PriorityNodeQueue.cs
@@ -0,0 +1,208 @@
+namespace Rin.Core.Collections;
+
+///
+/// Implements a priority queue of type T.
+/// Elements may be added to the queue in any order, but when we pull
+/// elements out of the queue, they will be returned in 'ascending' order.
+/// Adding new elements into the queue may be done at any time, so this is
+/// useful to implement a dynamically growing and shrinking queue. Both adding
+/// an element and removing the first element are log(N) operations.
+/// The queue is implemented using a priority-heap data structure. For more
+/// details on this elegant and simple data structure see "Programming Pearls"
+/// in our library. The tree is implemented atop a list, where 2N and 2N+1 are
+/// the child nodes of node N. The tree is balanced and left-aligned so there
+/// are no 'holes' in this list.
+///
+/// Type T.
+public class PriorityNodeQueue {
+ /// The List we use for implementation.
+ readonly List> items = new();
+
+ // Used for comparing and sorting elements.
+ readonly IComparer comparer;
+
+ /// Returns the number of elements in the queue.
+ public int Count => items.Count;
+
+ /// Returns true if the queue is empty.
+ /// Trying to call Peek() or Next() on an empty queue will throw an exception.
+ /// Check using Empty first before calling these methods.
+ public bool Empty => items.Count == 0;
+
+ public PriorityNodeQueue(IComparer comparer) {
+ this.comparer = comparer;
+ }
+
+ public PriorityNodeQueue() {
+ comparer = Comparer.Default;
+ }
+
+ /// Clear all the elements from the priority queue
+ public void Clear() {
+ items.Clear();
+ }
+
+ ///
+ /// Removes the specified item.
+ ///
+ /// The item to remove.
+ public void Remove(PriorityQueueNode item) {
+ var index = item.Index;
+ if (index == -1) {
+ return;
+ }
+
+ // The element to return is of course the first element in the array,
+ // or the root of the tree. However, this will leave a 'hole' there. We
+ // fill up this hole with the last element from the array. This will
+ // break the heap property. So we bubble the element downwards by swapping
+ // it with it's lower child until it reaches it's correct level. The lower
+ // child (one of the original elements with index 1 or 2) will now be at the
+ // head of the queue (root of the tree).
+ var nMax = items.Count - 1;
+ var itemMax = items[nMax];
+ itemMax.Index = index;
+ var itemToRemove = items[index];
+ itemToRemove.Index = -1;
+ items[index] = itemMax;
+ items.RemoveAt(nMax); // Move the last element to the top
+
+ var p = index;
+ while (true) {
+ // c is the child we want to swap with. If there
+ // is no child at all, then the heap is balanced
+ var c = p * 2;
+ if (c >= nMax) {
+ break;
+ }
+
+ // If the second child is smaller than the first, that's the one
+ // we want to swap with this parent.
+ if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) {
+ c++;
+ }
+
+ // If the parent is already smaller than this smaller child, then
+ // we are done
+ if (comparer.Compare(items[p].Value, items[c].Value) <= 0) {
+ break;
+ }
+
+ // Otherwise, swap parent and child, and follow down the parent
+ var tmp = items[p];
+ var tmp2 = items[c];
+ tmp.Index = c;
+ tmp2.Index = p;
+ items[p] = tmp2;
+ items[c] = tmp;
+ p = c;
+ }
+
+ item.Index = -1;
+ }
+
+ /// Add an element to the priority queue - O(log(n)) time operation.
+ /// The item to be added to the queue
+ /// A node representing the item.
+ public PriorityQueueNode Enqueue(T item) {
+ var result = new PriorityQueueNode(item);
+ Enqueue(result);
+ return result;
+ }
+
+ /// Add an element to the priority queue - O(log(n)) time operation.
+ /// The item to be added to the queue
+ public void Enqueue(PriorityQueueNode item) {
+ if (item.Index != -1) {
+ throw new InvalidOperationException("Item belongs to another PriorityNodeQueue.");
+ }
+
+ // We add the item to the end of the list (at the bottom of the
+ // tree). Then, the heap-property could be violated between this element
+ // and it's parent. If this is the case, we swap this element with the
+ // parent (a safe operation to do since the element is known to be less
+ // than it's parent). Now the element move one level up the tree. We repeat
+ // this test with the element and it's new parent. The element, if lesser
+ // than everybody else in the tree will eventually bubble all the way up
+ // to the root of the tree (or the head of the list). It is easy to see
+ // this will take log(N) time, since we are working with a balanced binary
+ // tree.
+ var n = items.Count;
+ items.Add(item);
+ item.Index = n;
+ while (n != 0) {
+ var p = n / 2; // This is the 'parent' of this item
+ if (comparer.Compare(items[n].Value, items[p].Value) >= 0) {
+ break; // Item >= parent
+ }
+
+ // Swap item and parent
+ var tmp = items[n];
+ var tmp2 = items[p];
+ tmp2.Index = n;
+ tmp.Index = p;
+ items[n] = tmp2;
+ items[p] = tmp;
+
+ n = p; // And continue
+ }
+ }
+
+ /// Allows you to look at the first element waiting in the queue, without removing it.
+ /// This element will be the one that will be returned if you subsequently call Next().
+ public T Peek() => items[0].Value;
+
+ /// Removes and returns the first element from the queue (least element)
+ /// The first element in the queue, in ascending order.
+ public T Dequeue() {
+ // The element to return is of course the first element in the array,
+ // or the root of the tree. However, this will leave a 'hole' there. We
+ // fill up this hole with the last element from the array. This will
+ // break the heap property. So we bubble the element downwards by swapping
+ // it with it's lower child until it reaches it's correct level. The lower
+ // child (one of the original elements with index 1 or 2) will now be at the
+ // head of the queue (root of the tree).
+ var nMax = items.Count - 1;
+ var itemToRemove = items[0];
+ var itemMax = items[nMax];
+ itemMax.Index = 0;
+ var val = itemToRemove.Value;
+ itemToRemove.Index = -1;
+ items[0] = itemMax;
+ items.RemoveAt(nMax); // Move the last element to the top
+
+ var p = 0;
+ while (true) {
+ // c is the child we want to swap with. If there
+ // is no child at all, then the heap is balanced
+ var c = p * 2;
+ if (c >= nMax) {
+ break;
+ }
+
+ // If the second child is smaller than the first, that's the one
+ // we want to swap with this parent.
+ if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) {
+ c++;
+ }
+
+ // If the parent is already smaller than this smaller child, then
+ // we are done
+ if (comparer.Compare(items[p].Value, items[c].Value) <= 0) {
+ break;
+ }
+
+ // Otherwise, swap parent and child, and follow down the parent
+ var tmp = items[p];
+ var tmp2 = items[c];
+ tmp.Index = c;
+ tmp2.Index = p;
+ items[p] = tmp2;
+ items[c] = tmp;
+
+ p = c;
+ }
+
+ return val;
+ }
+}
diff --git a/Rin.Core/Collections/PriorityQueueNode.cs b/Rin.Core/Collections/PriorityQueueNode.cs
new file mode 100644
index 0000000..a05e2fe
--- /dev/null
+++ b/Rin.Core/Collections/PriorityQueueNode.cs
@@ -0,0 +1,15 @@
+namespace Rin.Core.Collections;
+
+///
+/// Represents a node in a priority queue, to allow O(n) removal.
+///
+///
+public class PriorityQueueNode {
+ public T Value;
+ public int Index { get; internal set; }
+
+ public PriorityQueueNode(T value) {
+ Value = value;
+ Index = -1;
+ }
+}
diff --git a/Rin.Core/Collections/SortedList.cs b/Rin.Core/Collections/SortedList.cs
new file mode 100644
index 0000000..9a501a9
--- /dev/null
+++ b/Rin.Core/Collections/SortedList.cs
@@ -0,0 +1,1056 @@
+using System.Collections;
+using System.Diagnostics;
+
+namespace Rin.Core.Collections;
+
+///
+/// Represents a collection of associated keys and values
+/// that are sorted by the keys and are accessible by key
+/// and by index.
+///
+[DebuggerDisplay("Count = {" + nameof(Count) + "}")]
+public class SortedList : IDictionary, IDictionary {
+ static readonly int INITIAL_SIZE = 16;
+
+ int modificationCount;
+ KeyValuePair[] table;
+ int defaultCapacity;
+
+ public int Count { get; private set; }
+
+ public int Capacity {
+ get => table.Length;
+
+ set {
+ var current = table.Length;
+
+ if (Count > value) {
+ throw new ArgumentOutOfRangeException("capacity too small");
+ }
+
+ if (value == 0) {
+ // return to default size
+ var newTable = new KeyValuePair[defaultCapacity];
+ Array.Copy(table, newTable, Count);
+ table = newTable;
+ } else if (value > Count) {
+ var newTable = new KeyValuePair[value];
+ Array.Copy(table, newTable, Count);
+ table = newTable;
+ } else if (value > current) {
+ var newTable = new KeyValuePair[value];
+ Array.Copy(table, newTable, current);
+ table = newTable;
+ }
+ }
+ }
+
+ public IList Keys => new ListKeys(this);
+ public IList Values => new ListValues(this);
+ public IComparer Comparer { get; private set; }
+ bool ICollection.IsSynchronized => false;
+ object ICollection.SyncRoot => this;
+
+ // IDictionary
+
+ bool IDictionary.IsFixedSize => false;
+ bool IDictionary.IsReadOnly => false;
+ ICollection IDictionary.Keys => new ListKeys(this);
+ ICollection IDictionary.Values => new ListValues(this);
+ ICollection IDictionary.Keys => Keys;
+ ICollection IDictionary.Values => Values;
+ bool ICollection>.IsReadOnly => false;
+
+ public TValue this[TKey key] {
+ get {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var i = Find(key);
+
+ if (i >= 0) {
+ return table[i].Value;
+ }
+
+ throw new KeyNotFoundException();
+ }
+ set {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ PutImpl(key, value, true);
+ }
+ }
+
+ object IDictionary.this[object key] {
+ get {
+ if (key is not TKey key1) {
+ return null;
+ }
+
+ return this[key1];
+ }
+
+ set => this[ToKey(key)] = ToValue(value);
+ }
+
+ //
+ // Constructors
+ //
+ public SortedList()
+ : this(INITIAL_SIZE, null) { }
+
+ public SortedList(int capacity)
+ : this(capacity, null) { }
+
+ public SortedList(int capacity, IComparer comparer) {
+ if (capacity < 0) {
+ throw new ArgumentOutOfRangeException(nameof(capacity));
+ }
+
+ defaultCapacity = capacity == 0 ? 0 : INITIAL_SIZE;
+ Init(comparer, capacity, true);
+ }
+
+ public SortedList(IComparer comparer)
+ : this(INITIAL_SIZE, comparer) { }
+
+ public SortedList(IDictionary dictionary)
+ : this(dictionary, null) { }
+
+ public SortedList(IDictionary dictionary, IComparer comparer) {
+ if (dictionary == null) {
+ throw new ArgumentNullException(nameof(dictionary));
+ }
+
+ Init(comparer, dictionary.Count, true);
+
+ foreach (var kvp in dictionary) {
+ Add(kvp.Key, kvp.Value);
+ }
+ }
+
+ //
+ // Public instance methods.
+ //
+
+ public void Add(TKey key, TValue value) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ PutImpl(key, value, false);
+ }
+
+ public bool ContainsKey(TKey key) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return Find(key) >= 0;
+ }
+
+ public bool Remove(TKey key) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var i = IndexOfKey(key);
+ if (i >= 0) {
+ RemoveAt(i);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Clear() {
+ defaultCapacity = INITIAL_SIZE;
+ table = new KeyValuePair[defaultCapacity];
+ Count = 0;
+ modificationCount++;
+ }
+
+ // IEnumerable>
+
+ public Enumerator GetEnumerator() => new(this);
+
+ //
+ // SortedList
+ //
+
+ public void RemoveAt(int index) {
+ var table = this.table;
+ var cnt = Count;
+ if (index >= 0 && index < cnt) {
+ if (index != cnt - 1) {
+ Array.Copy(table, index + 1, table, index, cnt - 1 - index);
+ } else {
+ table[index] = default;
+ }
+
+ --Count;
+ ++modificationCount;
+ } else {
+ throw new ArgumentOutOfRangeException("index out of range");
+ }
+ }
+
+ public int IndexOfKey(TKey key) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var indx = 0;
+ try {
+ indx = Find(key);
+ } catch (Exception) {
+ throw new InvalidOperationException();
+ }
+
+ return indx | (indx >> 31);
+ }
+
+ public int IndexOfValue(TValue value) {
+ if (Count == 0) {
+ return -1;
+ }
+
+ for (var i = 0; i < Count; i++) {
+ var current = table[i];
+
+ if (Equals(value, current.Value)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public bool ContainsValue(TValue value) => IndexOfValue(value) >= 0;
+
+ public void TrimExcess() {
+ if (Count < table.Length * 0.9) {
+ Capacity = Count;
+ }
+ }
+
+ public bool TryGetValue(TKey key, out TValue value) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var i = Find(key);
+
+ if (i >= 0) {
+ value = table[i].Value;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ // ICollection>
+
+ void ICollection>.Clear() {
+ defaultCapacity = INITIAL_SIZE;
+ table = new KeyValuePair[defaultCapacity];
+ Count = 0;
+ modificationCount++;
+ }
+
+ void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) {
+ if (Count == 0) {
+ return;
+ }
+
+ if (null == array) {
+ throw new ArgumentNullException();
+ }
+
+ if (arrayIndex < 0) {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (arrayIndex >= array.Length) {
+ throw new ArgumentNullException("arrayIndex is greater than or equal to array.Length");
+ }
+
+ if (Count > array.Length - arrayIndex) {
+ throw new ArgumentNullException("Not enough space in array from arrayIndex to end of array");
+ }
+
+ var i = arrayIndex;
+ foreach (var pair in this) {
+ array[i++] = pair;
+ }
+ }
+
+ void ICollection>.Add(KeyValuePair keyValuePair) {
+ Add(keyValuePair.Key, keyValuePair.Value);
+ }
+
+ bool ICollection>.Contains(KeyValuePair keyValuePair) {
+ var i = Find(keyValuePair.Key);
+
+ if (i >= 0) {
+ return Comparer>.Default.Compare(table[i], keyValuePair) == 0;
+ }
+
+ return false;
+ }
+
+ bool ICollection>.Remove(KeyValuePair keyValuePair) {
+ var i = Find(keyValuePair.Key);
+
+ if (i >= 0 && Comparer>.Default.Compare(table[i], keyValuePair) == 0) {
+ RemoveAt(i);
+ return true;
+ }
+
+ return false;
+ }
+
+ IEnumerator> IEnumerable>.GetEnumerator() =>
+ new Enumerator(this);
+
+ // IEnumerable
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ // IDictionary
+
+ void IDictionary.Add(object key, object value) {
+ PutImpl(ToKey(key), ToValue(value), false);
+ }
+
+ bool IDictionary.Contains(object key) {
+ if (null == key) {
+ throw new ArgumentNullException();
+ }
+
+ if (!(key is TKey)) {
+ return false;
+ }
+
+ return Find((TKey)key) >= 0;
+ }
+
+ IDictionaryEnumerator IDictionary.GetEnumerator() => new DictionaryEnumerator(this, EnumeratorMode.ENTRY_MODE);
+
+ void IDictionary.Remove(object key) {
+ if (null == key) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (!(key is TKey)) {
+ return;
+ }
+
+ var i = IndexOfKey((TKey)key);
+ if (i >= 0) {
+ RemoveAt(i);
+ }
+ }
+
+ // ICollection
+
+ void ICollection.CopyTo(Array array, int arrayIndex) {
+ if (Count == 0) {
+ return;
+ }
+
+ if (null == array) {
+ throw new ArgumentNullException();
+ }
+
+ if (arrayIndex < 0) {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (array.Rank > 1) {
+ throw new ArgumentException("array is multi-dimensional");
+ }
+
+ if (arrayIndex >= array.Length) {
+ throw new ArgumentNullException("arrayIndex is greater than or equal to array.Length");
+ }
+
+ if (Count > array.Length - arrayIndex) {
+ throw new ArgumentNullException("Not enough space in array from arrayIndex to end of array");
+ }
+
+ IEnumerator> it = GetEnumerator();
+ var i = arrayIndex;
+
+ while (it.MoveNext()) {
+ array.SetValue(it.Current, i++);
+ }
+ }
+
+ //
+ // Private methods
+ //
+
+ void EnsureCapacity(int n, int free) {
+ var table = this.table;
+ KeyValuePair[] newTable = null;
+ var cap = Capacity;
+ var gap = free >= 0 && free < Count;
+
+ if (n > cap) {
+ newTable = new KeyValuePair[n << 1];
+ }
+
+ if (newTable != null) {
+ if (gap) {
+ var copyLen = free;
+ if (copyLen > 0) {
+ Array.Copy(table, 0, newTable, 0, copyLen);
+ }
+
+ copyLen = Count - free;
+ if (copyLen > 0) {
+ Array.Copy(table, free, newTable, free + 1, copyLen);
+ }
+ } else {
+ // Just a resizing, copy the entire table.
+ Array.Copy(table, newTable, Count);
+ }
+
+ this.table = newTable;
+ } else if (gap) {
+ Array.Copy(table, free, table, free + 1, Count - free);
+ }
+ }
+
+ void PutImpl(TKey key, TValue value, bool overwrite) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var table = this.table;
+
+ var freeIndx = -1;
+
+ try {
+ freeIndx = Find(key);
+ } catch (Exception) {
+ throw new InvalidOperationException();
+ }
+
+ if (freeIndx >= 0) {
+ if (!overwrite) {
+ throw new ArgumentException("element already exists");
+ }
+
+ table[freeIndx] = new(key, value);
+ ++modificationCount;
+ return;
+ }
+
+ freeIndx = ~freeIndx;
+
+ if (freeIndx > Capacity + 1) {
+ throw new("SortedList::internal error (" + key + ", " + value + ") at [" + freeIndx + "]");
+ }
+
+
+ EnsureCapacity(Count + 1, freeIndx);
+
+ table = this.table;
+ table[freeIndx] = new(key, value);
+
+ ++Count;
+ ++modificationCount;
+ }
+
+ void Init(IComparer comparer, int capacity, bool forceSize) {
+ if (comparer == null) {
+ comparer = Comparer.Default;
+ }
+
+ Comparer = comparer;
+ if (!forceSize && capacity < defaultCapacity) {
+ capacity = defaultCapacity;
+ }
+
+ table = new KeyValuePair[capacity];
+ Count = 0;
+ modificationCount = 0;
+ }
+
+ void CopyToArray(
+ Array arr,
+ int i,
+ EnumeratorMode mode
+ ) {
+ if (arr == null) {
+ throw new ArgumentNullException(nameof(arr));
+ }
+
+ if (i < 0 || i + Count > arr.Length) {
+ throw new ArgumentOutOfRangeException(nameof(i));
+ }
+
+ IEnumerator it = new DictionaryEnumerator(this, mode);
+
+ while (it.MoveNext()) {
+ arr.SetValue(it.Current, i++);
+ }
+ }
+
+ int Find(TKey key) {
+ var table = this.table;
+ var len = Count;
+
+ if (len == 0) {
+ return ~0;
+ }
+
+ var left = 0;
+ var right = len - 1;
+
+ while (left <= right) {
+ var guess = (left + right) >> 1;
+
+ var cmp = Comparer.Compare(table[guess].Key, key);
+ if (cmp == 0) {
+ return guess;
+ }
+
+ if (cmp < 0) {
+ left = guess + 1;
+ } else {
+ right = guess - 1;
+ }
+ }
+
+ return ~left;
+ }
+
+ TKey ToKey(object key) {
+ if (key == null) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (!(key is TKey)) {
+ throw new ArgumentException(
+ "The value \""
+ + key
+ + "\" isn't of type \""
+ + typeof(TKey)
+ + "\" and can't be used in this generic collection.",
+ nameof(key)
+ );
+ }
+
+ return (TKey)key;
+ }
+
+ TValue ToValue(object value) {
+ if (!(value is TValue)) {
+ throw new ArgumentException(
+ "The value \""
+ + value
+ + "\" isn't of type \""
+ + typeof(TValue)
+ + "\" and can't be used in this generic collection.",
+ nameof(value)
+ );
+ }
+
+ return (TValue)value;
+ }
+
+ enum EnumeratorMode {
+ KEY_MODE = 0,
+ VALUE_MODE,
+ ENTRY_MODE
+ }
+
+ internal TKey KeyAt(int index) {
+ if (index >= 0 && index < Count) {
+ return table[index].Key;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ internal TValue ValueAt(int index) {
+ if (index >= 0 && index < Count) {
+ return table[index].Value;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ //
+ // Inner classes
+ //
+
+ public sealed class Enumerator : IEnumerator> {
+ SortedList host;
+ int pos = -1;
+
+ public KeyValuePair Current => host.table[pos];
+ object IEnumerator.Current => Current;
+
+ public Enumerator(SortedList host) {
+ this.host = host;
+ }
+
+ public void Dispose() {
+ host = null;
+ }
+
+ public bool MoveNext() => ++pos < host.Count;
+
+ public void Reset() {
+ throw new NotSupportedException();
+ }
+ }
+
+
+ sealed class DictionaryEnumerator : IDictionaryEnumerator, IEnumerator {
+ readonly SortedList host;
+ int stamp;
+ int pos;
+ int size;
+ readonly EnumeratorMode mode;
+
+ object currentKey;
+ object currentValue;
+
+ bool invalid;
+
+ static readonly string xstr = "SortedList.Enumerator: snapshot out of sync.";
+
+ public DictionaryEntry Entry {
+ get {
+ if (invalid || pos >= size || pos == -1) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ return new(
+ currentKey,
+ currentValue
+ );
+ }
+ }
+
+ public object Key {
+ get {
+ if (invalid || pos >= size || pos == -1) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ return currentKey;
+ }
+ }
+
+ public object Value {
+ get {
+ if (invalid || pos >= size || pos == -1) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ return currentValue;
+ }
+ }
+
+ public object Current {
+ get {
+ if (invalid || pos >= size || pos == -1) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ switch (mode) {
+ case EnumeratorMode.KEY_MODE:
+ return currentKey;
+ case EnumeratorMode.VALUE_MODE:
+ return currentValue;
+ case EnumeratorMode.ENTRY_MODE:
+ return Entry;
+
+ default:
+ throw new NotSupportedException(mode + " is not a supported mode.");
+ }
+ }
+ }
+
+ public DictionaryEnumerator(SortedList host, EnumeratorMode mode) {
+ this.host = host;
+ stamp = host.modificationCount;
+ size = host.Count;
+ this.mode = mode;
+ Reset();
+ }
+
+ public DictionaryEnumerator(SortedList host)
+ : this(host, EnumeratorMode.ENTRY_MODE) { }
+
+ public void Reset() {
+ if (host.modificationCount != stamp || invalid) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ pos = -1;
+ currentKey = null;
+ currentValue = null;
+ }
+
+ public bool MoveNext() {
+ if (host.modificationCount != stamp || invalid) {
+ throw new InvalidOperationException(xstr);
+ }
+
+ var table = host.table;
+
+ if (++pos < size) {
+ var entry = table[pos];
+
+ currentKey = entry.Key;
+ currentValue = entry.Value;
+ return true;
+ }
+
+ currentKey = null;
+ currentValue = null;
+ return false;
+ }
+
+ // ICloneable
+
+ public object Clone() =>
+ new DictionaryEnumerator(host, mode) {
+ stamp = stamp,
+ pos = pos,
+ size = size,
+ currentKey = currentKey,
+ currentValue = currentValue,
+ invalid = invalid
+ };
+ }
+
+ struct KeyEnumerator : IEnumerator, IDisposable {
+ const int NOT_STARTED = -2;
+
+ // this MUST be -1, because we depend on it in move next.
+ // we just decr the size, so, 0 - 1 == FINISHED
+ const int FINISHED = -1;
+
+ readonly SortedList l;
+ int idx;
+ readonly int ver;
+
+ public TKey Current {
+ get {
+ if (idx < 0) {
+ throw new InvalidOperationException();
+ }
+
+ return l.KeyAt(l.Count - 1 - idx);
+ }
+ }
+
+ object IEnumerator.Current => Current;
+
+ internal KeyEnumerator(SortedList l) {
+ this.l = l;
+ idx = NOT_STARTED;
+ ver = l.modificationCount;
+ }
+
+ public void Dispose() {
+ idx = NOT_STARTED;
+ }
+
+ public bool MoveNext() {
+ if (ver != l.modificationCount) {
+ throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");
+ }
+
+ if (idx == NOT_STARTED) {
+ idx = l.Count;
+ }
+
+ return idx != FINISHED && --idx != FINISHED;
+ }
+
+ void IEnumerator.Reset() {
+ if (ver != l.modificationCount) {
+ throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");
+ }
+
+ idx = NOT_STARTED;
+ }
+ }
+
+ struct ValueEnumerator : IEnumerator, IDisposable {
+ const int NOT_STARTED = -2;
+
+ // this MUST be -1, because we depend on it in move next.
+ // we just decr the size, so, 0 - 1 == FINISHED
+ const int FINISHED = -1;
+
+ readonly SortedList l;
+ int idx;
+ readonly int ver;
+
+ public TValue Current {
+ get {
+ if (idx < 0) {
+ throw new InvalidOperationException();
+ }
+
+ return l.ValueAt(l.Count - 1 - idx);
+ }
+ }
+
+ object IEnumerator.Current => Current;
+
+ internal ValueEnumerator(SortedList l) {
+ this.l = l;
+ idx = NOT_STARTED;
+ ver = l.modificationCount;
+ }
+
+ public void Dispose() {
+ idx = NOT_STARTED;
+ }
+
+ public bool MoveNext() {
+ if (ver != l.modificationCount) {
+ throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");
+ }
+
+ if (idx == NOT_STARTED) {
+ idx = l.Count;
+ }
+
+ return idx != FINISHED && --idx != FINISHED;
+ }
+
+ void IEnumerator.Reset() {
+ if (ver != l.modificationCount) {
+ throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");
+ }
+
+ idx = NOT_STARTED;
+ }
+ }
+
+ class ListKeys : IList, IReadOnlyList, ICollection, IEnumerable {
+ readonly SortedList host;
+
+ //
+ // ICollection
+ //
+
+ public virtual int Count => host.Count;
+
+ public virtual bool IsSynchronized => ((ICollection)host).IsSynchronized;
+
+ public virtual bool IsReadOnly => true;
+
+ public virtual object SyncRoot => ((ICollection)host).SyncRoot;
+
+ public virtual TKey this[int index] {
+ get => host.KeyAt(index);
+ set => throw new NotSupportedException("attempt to modify a key");
+ }
+
+ public ListKeys(SortedList host) {
+ if (host == null) {
+ throw new ArgumentNullException();
+ }
+
+ this.host = host;
+ }
+
+ // ICollection
+
+ public virtual void Add(TKey item) {
+ throw new NotSupportedException();
+ }
+
+ public virtual bool Remove(TKey key) => throw new NotSupportedException();
+
+ public virtual void Clear() {
+ throw new NotSupportedException();
+ }
+
+ public virtual void CopyTo(TKey[] array, int arrayIndex) {
+ if (host.Count == 0) {
+ return;
+ }
+
+ if (array == null) {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (arrayIndex < 0) {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (arrayIndex >= array.Length) {
+ throw new ArgumentOutOfRangeException("arrayIndex is greater than or equal to array.Length");
+ }
+
+ if (Count > array.Length - arrayIndex) {
+ throw new ArgumentOutOfRangeException("Not enough space in array from arrayIndex to end of array");
+ }
+
+ var j = arrayIndex;
+ for (var i = 0; i < Count; ++i) {
+ array[j++] = host.KeyAt(i);
+ }
+ }
+
+ public virtual bool Contains(TKey item) => host.IndexOfKey(item) > -1;
+
+ //
+ // IList
+ //
+ public virtual int IndexOf(TKey item) => host.IndexOfKey(item);
+
+ public virtual void Insert(int index, TKey item) {
+ throw new NotSupportedException();
+ }
+
+ public virtual void RemoveAt(int index) {
+ throw new NotSupportedException();
+ }
+
+ //
+ // IEnumerable
+ //
+
+ public virtual IEnumerator GetEnumerator() =>
+ /* We couldn't use yield as it does not support Reset () */
+ new KeyEnumerator(host);
+
+ public virtual void CopyTo(Array array, int arrayIndex) {
+ host.CopyToArray(array, arrayIndex, EnumeratorMode.KEY_MODE);
+ }
+
+ //
+ // IEnumerable
+ //
+
+ IEnumerator IEnumerable.GetEnumerator() {
+ for (var i = 0; i < host.Count; ++i) {
+ yield return host.KeyAt(i);
+ }
+ }
+ }
+
+ class ListValues : IList, IReadOnlyList, ICollection, IEnumerable {
+ readonly SortedList host;
+
+ //
+ // ICollection
+ //
+
+ public virtual int Count => host.Count;
+
+ public virtual bool IsSynchronized => ((ICollection)host).IsSynchronized;
+
+ public virtual bool IsReadOnly => true;
+
+ public virtual object SyncRoot => ((ICollection)host).SyncRoot;
+
+ public virtual TValue this[int index] {
+ get => host.ValueAt(index);
+ set => throw new NotSupportedException("attempt to modify a key");
+ }
+
+ public ListValues(SortedList host) {
+ if (host == null) {
+ throw new ArgumentNullException();
+ }
+
+ this.host = host;
+ }
+
+ // ICollection
+
+ public virtual void Add(TValue item) {
+ throw new NotSupportedException();
+ }
+
+ public virtual bool Remove(TValue value) => throw new NotSupportedException();
+
+ public virtual void Clear() {
+ throw new NotSupportedException();
+ }
+
+ public virtual void CopyTo(TValue[] array, int arrayIndex) {
+ if (host.Count == 0) {
+ return;
+ }
+
+ if (array == null) {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (arrayIndex < 0) {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (arrayIndex >= array.Length) {
+ throw new ArgumentOutOfRangeException("arrayIndex is greater than or equal to array.Length");
+ }
+
+ if (Count > array.Length - arrayIndex) {
+ throw new ArgumentOutOfRangeException("Not enough space in array from arrayIndex to end of array");
+ }
+
+ var j = arrayIndex;
+ for (var i = 0; i < Count; ++i) {
+ array[j++] = host.ValueAt(i);
+ }
+ }
+
+ public virtual bool Contains(TValue item) => host.IndexOfValue(item) > -1;
+
+ //
+ // IList
+ //
+ public virtual int IndexOf(TValue item) => host.IndexOfValue(item);
+
+ public virtual void Insert(int index, TValue item) {
+ throw new NotSupportedException();
+ }
+
+ public virtual void RemoveAt(int index) {
+ throw new NotSupportedException();
+ }
+
+ //
+ // IEnumerable
+ //
+
+ public virtual IEnumerator GetEnumerator() =>
+ /* We couldn't use yield as it does not support Reset () */
+ new ValueEnumerator(host);
+
+ public virtual void CopyTo(Array array, int arrayIndex) {
+ host.CopyToArray(array, arrayIndex, EnumeratorMode.VALUE_MODE);
+ }
+
+ //
+ // IEnumerable
+ //
+
+ IEnumerator IEnumerable.GetEnumerator() {
+ for (var i = 0; i < host.Count; ++i) {
+ yield return host.ValueAt(i);
+ }
+ }
+ }
+} // SortedList
diff --git a/Rin.Core/IIdentifiable.cs b/Rin.Core/IIdentifiable.cs
new file mode 100644
index 0000000..8c21b6b
--- /dev/null
+++ b/Rin.Core/IIdentifiable.cs
@@ -0,0 +1,12 @@
+namespace Rin.Core;
+
+///
+/// Base interface for all identifiable instances.
+///
+public interface IIdentifiable {
+ ///
+ /// Gets the id of this instance
+ ///
+ // [NonOverridable]
+ Guid Id { get; set; }
+}
diff --git a/Rin.Core/PropertyContainer.cs b/Rin.Core/PropertyContainer.cs
index 8b56013..b3ea855 100644
--- a/Rin.Core/PropertyContainer.cs
+++ b/Rin.Core/PropertyContainer.cs
@@ -1,5 +1,6 @@
using Rin.Core;
using Rin.Core.Serialization;
+using Rin.Core.Serialization.Serializers;
using System.Collections;
using System.Reflection;
diff --git a/Rin.Core/Rin.Core.csproj b/Rin.Core/Rin.Core.csproj
index 3f6ceff..f536504 100644
--- a/Rin.Core/Rin.Core.csproj
+++ b/Rin.Core/Rin.Core.csproj
@@ -19,4 +19,19 @@
+
+
+
+ TextTemplatingFileGenerator
+ MemberSerializerGenerated.cs
+
+
+
+
+
+ True
+ True
+ MemberSerializerGenerated.tt
+
+
diff --git a/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs b/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs
new file mode 100644
index 0000000..3264420
--- /dev/null
+++ b/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs
@@ -0,0 +1,29 @@
+namespace Rin.Core.Serialization;
+
+///
+/// Declares a serializer like or , but
+/// externally.
+///
+[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
+public class DataSerializerGlobalAttribute : Attribute {
+ public string Profile { get; set; }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// either by its serializer type (it will act like and guess data type from the
+ /// generic type of ) or the data type (it will act just like if
+ /// was set on the data type).
+ ///
+ /// The serializer type. Can be null if if set.
+ /// The data type. Can be null if is set.
+ /// Defines how generic type are added to .
+ /// Similar to
+ /// True if it should use the auto-generated serializer.
+ public DataSerializerGlobalAttribute(
+ Type serializerType,
+ Type? dataType = null,
+ DataSerializerGenericMode mode = DataSerializerGenericMode.None,
+ bool inherited = false,
+ bool complexSerializer = false
+ ) { }
+}
diff --git a/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs b/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs
new file mode 100644
index 0000000..a7f0f33
--- /dev/null
+++ b/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs
@@ -0,0 +1,18 @@
+namespace Rin.Core.Serialization;
+
+///
+/// Allows enumeration of required data serializers.
+///
+public interface IDataSerializerGenericInstantiation {
+ ///
+ /// Enumerates required required by this instance of DataSerializer.
+ ///
+ ///
+ /// The code won't be executed, it will only be scanned for typeof() operands by the assembly processor.
+ /// Null is authorized in enumeration (for now).
+ ///
+ ///
+ ///
+ ///
+ void EnumerateGenericInstantiations(SerializerSelector serializerSelector, IList genericInstantiations);
+}
diff --git a/Rin.Core/Serialization/MemberSerializer.cs b/Rin.Core/Serialization/MemberSerializer.cs
new file mode 100644
index 0000000..b4632d6
--- /dev/null
+++ b/Rin.Core/Serialization/MemberSerializer.cs
@@ -0,0 +1,96 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Rin.Core.Serialization;
+
+public class MemberSerializer {
+ public static readonly Dictionary CachedTypes = new();
+ public static readonly Dictionary ReverseCachedTypes = new();
+
+ // Holds object references during serialization, useful when same object is referenced multiple time in same serialization graph.
+ public static PropertyKey> ObjectSerializeReferences = new(
+ "ObjectSerializeReferences",
+ typeof(SerializerExtensions),
+ DefaultValueMetadata.Delegate(
+ delegate {
+ return new Dictionary