diff --git a/.editorconfig b/.editorconfig
index 748e2e27..5f273c21 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -273,7 +273,9 @@ dotnet_diagnostic.CA2016.severity = warning
dotnet_diagnostic.CA2246.severity = warning
dotnet_diagnostic.IDE0004.severity = warning
dotnet_diagnostic.IDE0005.severity = warning
+dotnet_diagnostic.IDE0008.severity = none
dotnet_diagnostic.IDE0010.severity = none
+dotnet_diagnostic.IDE0011.severity = none
dotnet_diagnostic.IDE0016.severity = warning
dotnet_diagnostic.IDE0028.severity = warning
dotnet_diagnostic.IDE0029.severity = warning
@@ -285,6 +287,7 @@ dotnet_diagnostic.IDE0037.severity = warning
dotnet_diagnostic.IDE0041.severity = warning
dotnet_diagnostic.IDE0044.severity = warning
dotnet_diagnostic.IDE0054.severity = warning
+dotnet_diagnostic.IDE0061.severity = none
dotnet_diagnostic.IDE0062.severity = warning
dotnet_diagnostic.IDE0071.severity = warning
dotnet_diagnostic.IDE0082.severity = warning
diff --git a/.gitignore b/.gitignore
index 65b8307c..f19d6802 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ sarif-output
**/.DS_Store
BenchmarkDotNet*
+assetregistry-old
diff --git a/Directory.Build.props b/Directory.Build.props
index ea5cfb3d..2496f9e7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
g2c8a3f1b9e
0e92bb5
9b51cce
- 5c3471e
+ 331cf83
01e1238
4.9.0-$(VeldridHash)
1.0.1-$(VeldridHash)
diff --git a/extern/Mlang b/extern/Mlang
index 5c3471e8..331cf838 160000
--- a/extern/Mlang
+++ b/extern/Mlang
@@ -1 +1 @@
-Subproject commit 5c3471e8f025ac7600fba61e88a51e36c2b683b7
+Subproject commit 331cf838cf7cc0721d5ac8f7740c05915dde7c53
diff --git a/zzio.sln b/zzio.sln
index 64f4ecc4..165cf7df 100644
--- a/zzio.sln
+++ b/zzio.sln
@@ -29,6 +29,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0Common", "0Common", "{FB32
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
CodeQuality.props = CodeQuality.props
+ Directory.Build.props = Directory.Build.props
NoCodeQuality.props = NoCodeQuality.props
EndProjectSection
EndProject
diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs
new file mode 100644
index 00000000..a4e42c66
--- /dev/null
+++ b/zzre.core.tests/TestAssetRegistry.cs
@@ -0,0 +1,1974 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using DotNext;
+using DotNext.Collections.Generic;
+using NUnit.Framework;
+using NUnit.Framework.Constraints;
+
+namespace zzre.tests;
+
+[TestFixture(TaskContinuationOptions.None)]
+[TestFixture(TaskContinuationOptions.RunContinuationsAsynchronously)]
+[TestFixture(TaskContinuationOptions.ExecuteSynchronously)]
+[CancelAfter(10000), SingleThreaded]
+public class TestAssetRegistry
+{
+ private interface ITestAsset : IAsset
+ {
+ public IAssetRegistry Registry { get; init; }
+ public TestInfo Info { get; init; }
+ public int Id => Info.Id;
+ }
+
+ private readonly struct TestInfo(TaskContinuationOptions tcsOptions, int Id) : IEquatable
+ {
+ public readonly int Id = Id;
+ public readonly TaskCompletionSource StartedLoad = new(tcsOptions);
+ public readonly TaskCompletionSource FinishLoad = new(tcsOptions);
+ public readonly TaskCompletionSource Disposed = new(tcsOptions);
+
+ public TestInfo(TaskContinuationOptions tcsOptions, int Id, IAssetHandle[] secondaries)
+ : this(tcsOptions, Id) { }
+
+ public readonly TestInfo AsCompleted()
+ {
+ FinishLoad.SetResult();
+ return this;
+ }
+
+ public readonly TestInfo AsErroneous()
+ {
+ FinishLoad.SetException(new TestException());
+ return this;
+ }
+
+ public static async Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct)
+ where TAsset : ITestAsset, new()
+ {
+ ct.ThrowIfCancellationRequested();
+ Assert.That(info.StartedLoad.TrySetResult(), $"Asset {info.Id} was tried to be loaded twice");
+ await info.FinishLoad.Task.WaitAsync(ct);
+ ct.ThrowIfCancellationRequested();
+ return new(new TAsset() { Info = info, Registry = registry });
+ }
+
+ public bool Equals(TestInfo other) => Id == other.Id;
+ public override bool Equals([NotNullWhen(true)] object? obj) => obj is TestInfo other ? Equals(other) : false;
+ public override int GetHashCode() => Id.GetHashCode();
+ public override string ToString() => $"Test {Id}";
+ }
+
+ private class GlobalTestAsset : ITestAsset
+ {
+ public static AssetLocality Locality => AssetLocality.Global;
+ public IAssetRegistry Registry { get; init; }
+ public TestInfo Info { get; init; }
+
+ public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct)
+ => TestInfo.LoadAsync(registry, info, ct);
+
+ public void Dispose()
+ {
+ Volatile.Write(ref WasDisposed, true);
+ Info.Disposed.TrySetResult();
+ }
+ public bool WasDisposed;
+ }
+
+ private class GlobalMTDTestAsset : ITestAsset
+ {
+ public static bool NeedsMainThreadDisposal => true;
+ public static AssetLocality Locality => AssetLocality.Global;
+ public IAssetRegistry Registry { get; init; }
+ public TestInfo Info { get; init; }
+
+ public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct)
+ => TestInfo.LoadAsync(registry, info, ct);
+
+ public void Dispose()
+ {
+ if (!Registry.IsMainThread)
+ Info.Disposed.TrySetException(new AssertionException("MTD asset was not disposed on main thread"));
+ Volatile.Write(ref WasDisposed, true);
+ Info.Disposed.TrySetResult();
+ }
+ public bool WasDisposed;
+ }
+
+ private class LocalTestAsset : ITestAsset
+ {
+ public static AssetLocality Locality => AssetLocality.Local;
+ public IAssetRegistry Registry { get; init; }
+ public TestInfo Info { get; init; }
+
+ public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct)
+ => TestInfo.LoadAsync(registry, info, ct);
+
+ public void Dispose()
+ {
+ Volatile.Write(ref WasDisposed, true);
+ Info.Disposed.TrySetResult();
+ }
+ public bool WasDisposed;
+ }
+
+ private class UniqueTestAsset : ITestAsset
+ {
+ public static AssetLocality Locality => AssetLocality.Unique;
+ public IAssetRegistry Registry { get; init; }
+ public TestInfo Info { get; init; }
+
+ public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct)
+ => TestInfo.LoadAsync(registry, info, ct);
+
+ public void Dispose()
+ {
+ Volatile.Write(ref WasDisposed, true);
+ Info.Disposed.TrySetResult();
+ }
+ public bool WasDisposed;
+ }
+
+ private readonly TagContainer DI = new();
+ private readonly TaskContinuationOptions tcsOptions;
+
+ public TestAssetRegistry(TaskContinuationOptions tcsOptions) =>
+ this.tcsOptions = tcsOptions;
+
+ private TestInfo GetInfo(int id) =>
+ new TestInfo(tcsOptions, id);
+
+ [Test]
+ public void EmptyRegistries()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+ using var local2 = new AssetRegistry(DI, global);
+
+ Assert.That(global.IsLocalRegistry, Is.False);
+ Assert.That(local.IsLocalRegistry, Is.True);
+ Assert.That(local2.IsLocalRegistry, Is.True);
+ Assert.That(global.DIContainer, Is.SameAs(DI));
+ }
+
+ [Test]
+ public void RegistryWithLogger()
+ {
+ DI.AddTag(Serilog.Core.Logger.None);
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global, "local registry");
+ }
+
+ [Test]
+ public void CannotTwiceLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ Assert.That(() => new AssetRegistry(DI, local), Throws.Exception);
+ }
+
+ [Test]
+ public void RegistryDisposeOrder()
+ {
+ var global = new AssetRegistry(DI);
+ var local = new AssetRegistry(DI, global);
+ local.Dispose();
+ global.Dispose();
+
+ global = new AssetRegistry(DI);
+ local = new AssetRegistry(DI, global);
+ global.Dispose();
+ local.Dispose();
+
+ global = new AssetRegistry(DI);
+ local = new AssetRegistry(DI, global);
+ var local2 = new AssetRegistry(DI, global);
+ local.Dispose();
+ global.Dispose();
+ local2.Dispose();
+ }
+
+ [Test]
+ public async Task UpdateOnNonMainThread(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ await Assert.ThatAsync(() => Task.Run(global.Update), Throws.InvalidOperationException);
+ }
+
+ private void CommonAssetChecks(IAssetRegistry registry, AssetHandle handle, int id, TAsset? extAsset = null)
+ where TAsset : class, ITestAsset => Assert.Multiple(() =>
+ {
+ if (extAsset is not null)
+ Assert.That(handle.Asset, Is.SameAs(extAsset));
+ Assert.That(handle.Asset, Is.Not.Null);
+ Assert.That(handle.Asset.Id, Is.EqualTo(id));
+ Assert.That(handle.Asset.Registry, Is.SameAs(registry));
+ Assert.That(handle.Registry, Is.SameAs(registry));
+ });
+
+ [Test]
+ public void LoadSync_Single()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ CommonAssetChecks(global, handle, 1);
+ }
+
+ [Test]
+ public void LoadSync_MultipleDiff()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ using var handle2 = global.Load(GetInfo(42).AsCompleted(), AssetPriority.Synchronous);
+ using var handle3 = global.Load(GetInfo(1337).AsCompleted(), AssetPriority.Synchronous);
+ CommonAssetChecks(global, handle1, 1);
+ CommonAssetChecks(global, handle2, 42);
+ CommonAssetChecks(global, handle3, 1337);
+ }
+
+ [Test]
+ public void LoadSync_MultipleSame()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ using var handle2 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ using var handle3 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ CommonAssetChecks(global, handle1, 1);
+ CommonAssetChecks(global, handle2, 1);
+ CommonAssetChecks(global, handle3, 1);
+
+ Assert.That(handle1, Is.EqualTo(handle2));
+ Assert.That(handle1, Is.EqualTo(handle3));
+ Assert.That(handle1.Asset, Is.SameAs(handle2.Asset));
+ Assert.That(handle1.Asset, Is.SameAs(handle3.Asset));
+ }
+
+ [Test]
+ public async Task LoadHigh_Single(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1); // uncompleted
+ using var handle = global.Load(info, AssetPriority.High);
+
+ Assert.That(handle.Asset, Is.Null);
+ await info.StartedLoad.Task.WaitAsync(ct);
+ Assert.That(handle.Asset, Is.Null);
+ info.FinishLoad.SetResult();
+ var asset = await handle.GetAsync(ct);
+ CommonAssetChecks(global, handle, 1, asset);
+ }
+
+ [Test]
+ public async Task LoadHigh_MultipleDiff(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info1 = GetInfo(1);
+ var info2 = GetInfo(2);
+ var info3 = GetInfo(3);
+ using var handle1 = global.Load(info1, AssetPriority.High);
+ using var handle2 = global.Load(info2, AssetPriority.High);
+ using var handle3 = global.Load(info3, AssetPriority.High);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info2.FinishLoad.SetResult();
+ var asset2 = await handle2.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 2, asset2);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Not.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info1.FinishLoad.SetResult();
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+
+ Assert.That(handle1.Asset, Is.Not.Null);
+ Assert.That(handle2.Asset, Is.Not.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info3.FinishLoad.SetResult();
+ var asset3 = await handle3.GetAsync(ct);
+ CommonAssetChecks(global, handle3, 3, asset3);
+ }
+
+ [Test]
+ public async Task LoadHigh_MultipleSame_Parallel1(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle1 = global.Load(info, AssetPriority.High);
+ using var handle2 = global.Load(info, AssetPriority.High);
+ using var handle3 = global.Load(info, AssetPriority.High);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info.FinishLoad.SetResult();
+
+ var asset2 = await handle2.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(handle1.Asset, Is.SameAs(asset2));
+ Assert.That(handle3.Asset, Is.SameAs(asset2));
+
+ var asset1 = await handle1.GetAsync(ct);
+ var asset3 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle3, 1, asset3);
+ }
+
+ [Test]
+ public async Task LoadHigh_MultipleSame_Parallel2(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle1 = global.Load(info, AssetPriority.High);
+ using var handle2 = global.Load(info, AssetPriority.High);
+ using var handle3 = global.Load(info, AssetPriority.High);
+
+ var waitTask = Task.WhenAll(
+ Task.Run(() => handle1.GetAsync(ct).AsTask(), ct),
+ Task.Run(() => handle2.GetAsync(ct).AsTask(), ct),
+ Task.Run(() => handle3.GetAsync(ct).AsTask(), ct));
+ info.FinishLoad.SetResult();
+ var results = await waitTask.WaitAsync(ct);
+
+ CommonAssetChecks(global, handle1, 1, results[0]);
+ CommonAssetChecks(global, handle1, 1, results[1]);
+ CommonAssetChecks(global, handle1, 1, results[2]);
+ }
+
+ [Test]
+ public async Task LoadHigh_MultipleSame_Sequential(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+
+ using var handle1 = global.Load(info, AssetPriority.High);
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+
+ using var handle2 = global.Load(info, AssetPriority.High);
+ var asset2 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(asset1, Is.SameAs(asset2));
+ }
+
+ [Test]
+ public async Task LoadHigh_MultipleSame_Interleaved(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle1 = global.Load(info, AssetPriority.High);
+ await info.StartedLoad.Task.WaitAsync(ct);
+ using var handle2 = global.Load(info, AssetPriority.High);
+ info.FinishLoad.SetResult();
+
+ var asset2 = await handle2.GetAsync(ct);
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(asset1, Is.SameAs(asset2));
+ }
+
+ [Test]
+ public void LoadHigh_AccessSync()
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+ info.FinishLoad.SetResult();
+
+ var asset = handle.Get();
+ CommonAssetChecks(global, handle, 1, asset);
+ }
+
+ [Test]
+ public async Task LoadHigh_ThenLoadSync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle1 = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task.WaitAsync(ct);
+ info.FinishLoad.SetResult();
+ using var handle2 = global.Load(info, AssetPriority.Synchronous);
+ var asset2 = handle2.Get();
+
+ CommonAssetChecks(global, handle2, 1, asset2);
+ }
+
+ [Test]
+ public async Task LoadLow_Single(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1); // uncompleted
+ using var handle = global.Load(info, AssetPriority.Low);
+
+ Assert.That(handle.Asset, Is.Null);
+ global.Update();
+ await info.StartedLoad.Task.WaitAsync(ct);
+ Assert.That(handle.Asset, Is.Null);
+ info.FinishLoad.SetResult();
+ var asset = await handle.GetAsync(ct);
+ CommonAssetChecks(global, handle, 1, asset);
+ }
+
+ [Test]
+ public async Task LoadLow_MultipleDiff(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info1 = GetInfo(1);
+ var info2 = GetInfo(2);
+ var info3 = GetInfo(3);
+ using var handle1 = global.Load(info1, AssetPriority.Low);
+ using var handle2 = global.Load(info2, AssetPriority.Low);
+ using var handle3 = global.Load(info3, AssetPriority.Low);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ global.Update();
+ info2.FinishLoad.SetResult();
+ var asset2 = await handle2.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 2, asset2);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Not.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info1.FinishLoad.SetResult();
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+
+ Assert.That(handle1.Asset, Is.Not.Null);
+ Assert.That(handle2.Asset, Is.Not.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ info3.FinishLoad.SetResult();
+ var asset3 = await handle3.GetAsync(ct);
+ CommonAssetChecks(global, handle3, 3, asset3);
+ }
+
+ [Test]
+ public async Task LoadLow_MultipleSame_Parallel1(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle1 = global.Load(info, AssetPriority.Low);
+ using var handle2 = global.Load(info, AssetPriority.Low);
+ using var handle3 = global.Load(info, AssetPriority.Low);
+
+ Assert.That(handle1.Asset, Is.Null);
+ Assert.That(handle2.Asset, Is.Null);
+ Assert.That(handle3.Asset, Is.Null);
+ global.Update();
+ info.FinishLoad.SetResult();
+
+ var asset2 = await handle2.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(handle1.Asset, Is.SameAs(asset2));
+ Assert.That(handle3.Asset, Is.SameAs(asset2));
+
+ var asset1 = await handle1.GetAsync(ct);
+ var asset3 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle3, 1, asset3);
+ }
+
+ [Test]
+ public async Task LoadLow_MultipleSame_Parallel2(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle1 = global.Load(info, AssetPriority.Low);
+ using var handle2 = global.Load(info, AssetPriority.Low);
+ using var handle3 = global.Load(info, AssetPriority.Low);
+
+ var waitTask = Task.WhenAll(
+ Task.Run(() => handle1.GetAsync(ct).AsTask(), ct),
+ Task.Run(() => handle2.GetAsync(ct).AsTask(), ct),
+ Task.Run(() => handle3.GetAsync(ct).AsTask(), ct));
+ global.Update();
+ info.FinishLoad.SetResult();
+ var results = await waitTask.WaitAsync(ct);
+
+ CommonAssetChecks(global, handle1, 1, results[0]);
+ CommonAssetChecks(global, handle1, 1, results[1]);
+ CommonAssetChecks(global, handle1, 1, results[2]);
+ }
+
+ [Test]
+ public async Task LoadLow_MultipleSame_Sequential(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+
+ using var handle1 = global.Load(info, AssetPriority.Low);
+ global.Update();
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+
+ using var handle2 = global.Load(info, AssetPriority.Low);
+ global.Update(); // should be noop
+ var asset2 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(asset1, Is.SameAs(asset2));
+ }
+
+ [Test]
+ public async Task LoadLow_MultipleSame_Interleaved(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle1 = global.Load(info, AssetPriority.Low);
+ global.Update();
+ await info.StartedLoad.Task.WaitAsync(ct);
+ using var handle2 = global.Load(info, AssetPriority.Low);
+ info.FinishLoad.SetResult();
+
+ var asset2 = await handle2.GetAsync(ct);
+ var asset1 = await handle1.GetAsync(ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ Assert.That(asset1, Is.SameAs(asset2));
+ }
+
+ [Test]
+ public async Task LoadLow_DelRefBeforeLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle1 = global.Load(info, AssetPriority.High);
+ using var handle2 = global.Load(info, AssetPriority.Low);
+
+ info.FinishLoad.SetResult();
+ var asset1 = await handle1.GetAsync(ct);
+ handle1.Dispose();
+ global.Update();
+
+ Assert.That(handle1.Get, Throws.InstanceOf());
+ Assert.That(asset1.WasDisposed, Is.False);
+ var asset2 = await handle2.GetAsync(ct);
+ Assert.That(asset1, Is.SameAs(asset2));
+ CommonAssetChecks(global, handle2, 1, asset2);
+ }
+
+ [Test]
+ public async Task LoadLow_DelRefDuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle = global.Load(info, AssetPriority.Low);
+ global.Update();
+ await info.StartedLoad.Task.WaitAsync(ct);
+ handle.Dispose();
+ info.FinishLoad.SetResult();
+
+ await Task.Delay(50);
+ global.Update();
+ }
+
+ [Test]
+ public void LoadLow_ThenGetSync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+
+ using var handle = global.Load(info, AssetPriority.Low);
+ var asset = handle.Get();
+ CommonAssetChecks(global, handle, 1, asset);
+
+ // check whether unnecessary low batch will break something
+ global.Update();
+ CommonAssetChecks(global, handle, 1, asset);
+ }
+
+ [Test]
+ public async Task LoadSequential([Values] AssetPriority prio1, [Values] AssetPriority prio2, CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ var (handle1, asset1) = await CommonLoadAsset1(global, prio1, ct);
+ var (handle2, asset2) = await CommonLoadAsset1(global, prio2, ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ }
+
+ [Test]
+ public async Task LoadSequential_WithDisposal([Values] AssetPriority prio1, [Values] AssetPriority prio2, CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ var (handle1, asset1) = await CommonLoadAsset1(global, prio1, ct);
+ CommonAssetChecks(global, handle1, 1, asset1);
+ handle1.Dispose();
+ Assert.That(asset1.WasDisposed, Is.True);
+
+ var (handle2, asset2) = await CommonLoadAsset1(global, prio2, ct);
+ CommonAssetChecks(global, handle2, 1, asset2);
+ handle2.Dispose();
+ Assert.That(asset2.WasDisposed, Is.True);
+
+ Assert.That(asset1, Is.Not.SameAs(asset2));
+ }
+
+ private Task<(AssetHandle, GlobalTestAsset)> CommonLoadAsset1(
+ AssetRegistry global,
+ AssetPriority prio,
+ CancellationToken ct) =>
+ CommonLoadAsset(global, GetInfo(1), prio, ct);
+
+ private async Task<(AssetHandle, GlobalTestAsset)> CommonLoadAsset(
+ AssetRegistry global,
+ TestInfo info,
+ AssetPriority prio,
+ CancellationToken ct)
+ {
+ if (prio is AssetPriority.Synchronous)
+ info.FinishLoad.TrySetResult();
+ var handle = global.Load(info, prio);
+
+ if (prio is AssetPriority.Synchronous)
+ return (handle, handle.Get());
+ if (prio is AssetPriority.Low)
+ global.Update();
+
+ info.FinishLoad.TrySetResult();
+ return (handle, await Task.Run(() => handle.GetAsync(ct).AsTask(), ct));
+ }
+
+ [Test]
+ public void DisposeRegistry_SyncAssset()
+ {
+ var global = new AssetRegistry(DI);
+ var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var asset = handle1.Get();
+ global.Dispose();
+
+ Assert.That(asset.WasDisposed, Is.True);
+ Assert.That(global.WasDisposed, Is.True);
+ Assert.That(handle1.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public async Task DisposeRegistry_HighAsset_DuringLoad(CancellationToken ct)
+ {
+ var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ var handle = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task.WaitAsync(ct);
+ global.Dispose();
+ info.FinishLoad.SetResult();
+
+ // we cannot guarantee that any actual dispose can be called if no asset
+ // reference was ever given to AssetRegistry.
+ // In cases of cancellation during asset load, the asset load has to make
+ // sure that disposal of objects is called.
+
+ //Assert.That(info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ Assert.That(handle.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public async Task DisposeRegistry_LowAsset_BeforeLoad()
+ {
+ var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ var handle = global.Load(info, AssetPriority.Low);
+
+ global.Dispose();
+ Assert.That(info.StartedLoad.Task.IsCompleted, Is.False);
+ Assert.That(handle.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public void DisposeRegistry_AccessAfter()
+ {
+ var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ global.Dispose();
+
+ Assert.That(handle.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public void DisposeRegistry_DisposeTwice()
+ {
+ var global = new AssetRegistry(DI);
+ global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ global.Load(GetInfo(2).AsCompleted(), AssetPriority.Synchronous);
+ global.Load(GetInfo(3).AsCompleted(), AssetPriority.Synchronous);
+
+ global.Dispose();
+ global.Dispose();
+ }
+
+ [Test]
+ public void DisposeAsset_AccessAfter()
+ {
+ using var global = new AssetRegistry(DI);
+ var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ handle.Dispose();
+
+ Assert.That(handle.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public async Task DisposeAsset_MultiRefs(
+ [Values] AssetPriority prio1,
+ [Values] AssetPriority prio2,
+ [Values] AssetPriority prio3,
+ CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var (handle1, asset1) = await CommonLoadAsset1(global, prio1, ct);
+ var (handle2, asset2) = await CommonLoadAsset1(global, prio2, ct);
+ var (handle3, asset3) = await CommonLoadAsset1(global, prio3, ct);
+ var asset = handle1.Asset;
+
+ Assert.That(asset.WasDisposed, Is.False);
+ handle1.Dispose();
+ Assert.That(asset.WasDisposed, Is.False);
+ handle3.Dispose();
+ Assert.That(asset.WasDisposed, Is.False);
+ handle2.Dispose();
+ Assert.That(asset.WasDisposed, Is.True);
+ }
+
+ [Test]
+ public async Task DisposeAsset_MultiThreaded([Values] bool needsMainThread, [Values] bool isMainThread, CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ IAssetHandle handle;
+ ITestAsset asset;
+ if (needsMainThread)
+ {
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ handle = thandle;
+ asset = thandle.Asset!;
+ }
+ else
+ {
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ handle = thandle;
+ asset = thandle.Asset!;
+ }
+ Assert.That(asset.Info.Disposed.Task.IsCompleted, Is.False);
+
+ if (isMainThread)
+ {
+ handle.Dispose();
+ Assert.That(asset.Info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ }
+ else if (needsMainThread)
+ {
+ await Task.Run(handle.Dispose);
+ Assert.That(asset.Info.Disposed.Task.IsCompleted, Is.False);
+ global.Update();
+ Assert.That(asset.Info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ }
+ else
+ {
+ await Task.Run(() =>
+ {
+ handle.Dispose();
+ Assert.That(asset.Info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ }, ct);
+ }
+ }
+
+ [Test, Repeat(50, StopOnFailure = true)]
+ public async Task DisposeAsset_StressBeforeLowLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var infos = Enumerable.Range(1, 8).Select(i => GetInfo(i).AsCompleted()).ToArray();
+ var handles = infos.Select(info =>
+ global.Load(info, AssetPriority.Low))
+ .ToArray();
+ global.Update();
+ foreach (var handle in handles)
+ handle.Dispose();
+ await UpdateAndCheckDisposal(global, infos, ct);
+ }
+
+ [Test, Repeat(50, StopOnFailure = true)]
+ public async Task DisposeAsset_StressBeforeHighLoad(CancellationToken ct)
+ {
+ Console.WriteLine("started run");
+ using var global = new AssetRegistry(DI);
+ await Task.WhenAll(Enumerable
+ .Range(1, 50)
+ .Select(i => Task.Run(() => SingularStress(i), ct))
+ ).WaitAsync(ct);
+ Console.WriteLine("ended run");
+
+ async Task SingularStress(int id)
+ {
+ var info = GetInfo(id).AsCompleted();
+ var handle = global.Load(info, AssetPriority.High);
+ handle.Dispose();
+ await Task.Yield();
+ if (info.StartedLoad.Task.IsCompleted)
+ await info.Disposed.Task.WaitAsync(ct);
+ // if the load has not started, the disposal will obviously also never happen
+ }
+ }
+
+ [Test]
+ public async Task DisposeAsset_BeforeHighLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+ var handle = global.Load(info, AssetPriority.High);
+ handle.Dispose();
+ await Task.Delay(50);
+ if (info.StartedLoad.Task.IsCompleted)
+ await info.Disposed.Task.WaitAsync(ct);
+ }
+
+ [Test]
+ public async Task DisposeAsset_DuringHighLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle1 = global.Load(info, AssetPriority.High);
+ await info.StartedLoad.Task.WaitAsync(ct);
+ handle1.Dispose();
+ info.FinishLoad.SetResult();
+
+ await info.Disposed.Task.WaitAsync(ct);
+ }
+
+ [Test]
+ public async Task DisposeAsset_DuringLowLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle1 = global.Load(info, AssetPriority.Low);
+ global.Update();
+ await info.StartedLoad.Task.WaitAsync(ct);
+ handle1.Dispose();
+ info.FinishLoad.SetResult();
+
+ await info.Disposed.Task.WaitAsync(ct);
+ }
+
+ [Test]
+ public async Task DisposeAsset_MTDAlreadyDead(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+
+ using var handle = global.Load(info, AssetPriority.High);
+ await info.StartedLoad.Task.WaitAsync(ct);
+ handle.Dispose();
+ info.FinishLoad.SetResult();
+
+ await UpdateAndCheckDisposal(global, [info], ct);
+ }
+
+ private async Task UpdateAndCheckDisposal(IAssetRegistry registry, TestInfo[] allInfos, CancellationToken ct)
+ {
+ var infos = allInfos.ToHashSet();
+ while (!ct.IsCancellationRequested && infos.Any())
+ {
+ await Task.Yield();
+ registry.Update();
+ infos.RemoveWhere(i =>
+ {
+ if (!i.StartedLoad.Task.IsCompleted)
+ return true;
+ if (!i.Disposed.Task.IsCompleted)
+ return false;
+ if (i.Disposed.Task.Exception is Exception e)
+ ExceptionDispatchInfo.Throw(e);
+ return true;
+ });
+ }
+ ct.ThrowIfCancellationRequested();
+ }
+
+ private sealed class TestException : Exception
+ {
+
+ }
+ private static InstanceOfTypeConstraint ThrowsAssetExceptions =>
+ Throws.InstanceOf();
+
+ [Test]
+ public void Error_SingleSync()
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+
+ Assert.That(() =>
+ {
+ global.Load(info, AssetPriority.Synchronous);
+ }, ThrowsAssetExceptions);
+ }
+
+ [Test]
+ public async Task Error_SingleHighUnobserved(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.High);
+ await info.StartedLoad.Task.WaitAsync(ct);
+ await Task.Delay(10); // ugly but no real way to check...
+ }
+
+ [Test]
+ public async Task Error_SingleHighGetAsync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await Assert.ThatAsync(async () =>
+ {
+ _ = await handle.GetAsync(ct);
+ }, ThrowsAssetExceptions);
+ await Assert.ThatAsync(async () =>
+ {
+ _ = await handle.GetAsync(ct);
+ }, ThrowsAssetExceptions);
+ }
+
+ [Test]
+ public void Error_SingleHighGetSync()
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.High);
+
+ Assert.That(() =>
+ {
+ _ = handle.Get();
+ }, ThrowsAssetExceptions);
+ Assert.That(() =>
+ {
+ _ = handle.Get();
+ }, ThrowsAssetExceptions);
+ }
+
+ [Test]
+ public async Task Error_ResetAfterDispose(
+ [Values(AssetPriority.Synchronous, AssetPriority.High)] AssetPriority priority,
+ [Values] bool getAsync,
+ CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ using (var handle1 = Load(true)) ;
+
+ using var handle2 = Load(false).Value;
+ var asset = getAsync
+ ? await handle2.GetAsync(ct)
+ : handle2.Get();
+ CommonAssetChecks(global, handle2, 1, asset);
+
+ AssetHandle? Load(bool withException)
+ {
+ var info = withException
+ ? GetInfo(1).AsErroneous()
+ : GetInfo(1).AsCompleted();
+ if (priority is AssetPriority.Synchronous && withException)
+ {
+ Assert.That(() => global.Load(info, priority),
+ ThrowsAssetExceptions);
+ return null;
+ }
+ else
+ return global.Load(info, priority);
+ }
+ }
+
+ [Test]
+ public async Task Error_NoResetWithRefs(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ var handle1 = global.Load(info, AssetPriority.High);
+ var handle2 = global.Load(info, AssetPriority.High);
+
+ await Assert.ThatAsync(async () => await handle1.GetAsync(ct), ThrowsAssetExceptions);
+
+ Assert.That(() =>
+ {
+ handle2.Get();
+ }, ThrowsAssetExceptions);
+
+ handle1.Dispose();
+
+ Assert.That(() =>
+ {
+ handle2.Get();
+ }, ThrowsAssetExceptions);
+ }
+
+ [Test]
+ public void Error_LoadLowThenGetSync()
+ {
+ // this triggers an otherwise uncovered line
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.Low);
+
+ Assert.That(() => handle.Get(), ThrowsAssetExceptions);
+ }
+
+ [Test]
+ public void Local_LoadLocalFromGlobal()
+ {
+ using var global = new AssetRegistry(DI);
+
+ Assert.That(() =>
+ {
+ global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ }, Throws.Exception);
+ }
+
+ [Test]
+ public void Local_LoadGlobalFromLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var asset = handle.Get();
+ CommonAssetChecks(global, handle, 1, asset);
+ }
+
+ [Test]
+ public void Local_LoadLocalFromLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var asset = handle.Get();
+ CommonAssetChecks(local, handle, 1, asset);
+ }
+
+ [Test]
+ public void Unique_LoadMultiple()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ using var handle1 = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ using var handle2 = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var asset1 = handle1.Get();
+ var asset2 = handle2.Get();
+
+ CommonAssetChecks(local, handle1, 1, asset1);
+ CommonAssetChecks(local, handle2, 1, asset2);
+ Assert.That(handle1, Is.Not.EqualTo(handle2));
+ Assert.That(asset1, Is.Not.SameAs(asset2));
+
+ handle1.Dispose();
+ Assert.That(handle2.Get, Throws.Nothing); // not disposed
+ }
+
+ [Test]
+ public async Task LoadNested_AsyncSecondaryHigh([Values] bool parentLow, CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ var parentInfo = GetInfo(1);
+ using var parentHandle = global.Load(parentInfo,
+ parentLow ? AssetPriority.Low : AssetPriority.High);
+ if (parentLow)
+ global.Update();
+
+ // We could load secondary low as the test setup would run the second .Update call on the main thread
+ // However that is not really a productive scenario
+ await parentInfo.StartedLoad.Task;
+ using var childHandle = global.Load(GetInfo(2).AsCompleted(), AssetPriority.High);
+ var childAsset = await childHandle.GetAsync(ct);
+ parentInfo.FinishLoad.SetResult();
+ var parentAsset = await parentHandle.GetAsync(ct);
+
+ CommonAssetChecks(global, parentHandle, 1, parentAsset);
+ CommonAssetChecks(global, childHandle, 2, childAsset);
+ }
+
+ [Test]
+ public async Task LoadNested_SyncSecondaryHigh(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ var parentInfo = GetInfo(1);
+ AssetHandle childHandle = default;
+ var loadChildTask = Task.Run(async () =>
+ {
+ await parentInfo.StartedLoad.Task;
+ childHandle = global.Load(GetInfo(2).AsCompleted(), AssetPriority.High);
+ await childHandle.GetAsync(ct);
+ parentInfo.FinishLoad.SetResult();
+ }, ct);
+
+ using var parentHandle = global.Load(parentInfo, AssetPriority.Synchronous);
+ CommonAssetChecks(global, parentHandle, 1);
+ CommonAssetChecks(global, childHandle, 2);
+
+ await loadChildTask.WaitAsync(ct); // just to be sure it *finished*
+ // the secondary should have been ready after that synchronous primary load
+ }
+
+ [Test]
+ public async Task LoadNested_Deep(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+
+ var info1 = GetInfo(1);
+ var info2 = GetInfo(2);
+ var info3 = GetInfo(3);
+ var info4 = GetInfo(4).AsCompleted();
+
+ using var handle1 = global.Load(info1, AssetPriority.High);
+ await info1.StartedLoad.Task;
+ using var handle2 = global.Load(info2, AssetPriority.High);
+ await info2.StartedLoad.Task;
+ using var handle3 = global.Load(info3, AssetPriority.High);
+ await info3.StartedLoad.Task;
+ using var handle4 = global.Load(info4, AssetPriority.High);
+ var asset4 = await handle4.GetAsync(ct);
+ info3.FinishLoad.SetResult();
+ var asset3 = await handle3.GetAsync(ct);
+ info2.FinishLoad.SetResult();
+ var asset2 = await handle2.GetAsync(ct);
+ info1.FinishLoad.SetResult();
+ var asset1 = await handle1.GetAsync(ct);
+
+ CommonAssetChecks(global, handle1, 1, asset1);
+ CommonAssetChecks(global, handle2, 2, asset2);
+ CommonAssetChecks(global, handle3, 3, asset3);
+ CommonAssetChecks(global, handle4, 4, asset4);
+ }
+
+ // We cannot detect recursive loads, so we cannot test for it...
+
+ [Test]
+ public async Task LoadNested_SyncDoesNotWork(CancellationToken ct)
+ {
+ // with actual asynchronous loading (so not in the test env main thread)
+ // no synchronous loading can be done as that would be main thread material
+
+ using var global = new AssetRegistry(DI);
+
+ AssetHandle childHandle = default;
+ var parentInfo = GetInfo(1);
+
+ var loadChildTask = Task.Run(async () =>
+ {
+ try
+ {
+ await parentInfo.StartedLoad.Task;
+ childHandle = global.Load(GetInfo(2).AsCompleted(), AssetPriority.Synchronous);
+ parentInfo.FinishLoad.SetResult();
+ }
+ catch (Exception e)
+ {
+ parentInfo.FinishLoad.SetException(e);
+ }
+ }, ct);
+
+ Assert.That(() => global.Load(parentInfo, AssetPriority.Synchronous),
+ Throws.InvalidOperationException);
+ // the exception would have been within the parent load so no disposal can be done by AssetRegistry
+ }
+
+ [Test]
+ public void Handle_Duplicate()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var info = GetInfo(1).AsCompleted();
+ var handle1 = global.Load(info, AssetPriority.Synchronous);
+ CommonAssetChecks(global, handle1, 1);
+
+ var handle2 = handle1.Duplicate();
+ CommonAssetChecks(global, handle2, 1);
+ Assert.That(handle2, Is.EqualTo(handle2));
+
+ handle1.Dispose();
+ Assert.That(info.Disposed.Task.IsCompleted, Is.False);
+
+ handle2.Dispose();
+ Assert.That(info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ }
+
+ [Test]
+ public void Handle_Move()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var info = GetInfo(1).AsCompleted();
+ var handle1 = global.Load(info, AssetPriority.Synchronous);
+ CommonAssetChecks(global, handle1, 1);
+
+ var handle2 = handle1.Move();
+ CommonAssetChecks(global, handle2, 1);
+
+ Assert.That(() => handle1.Asset, Throws.Exception);
+ Assert.That(() => handle1.Get(), Throws.Exception);
+ Assert.That(handle1, Is.EqualTo(handle2)); // equality does not change, Move=Duplicate+Dispose (conceptually)
+
+ handle1.Dispose();
+ Assert.That(info.Disposed.Task.IsCompleted, Is.False);
+
+ handle2.Dispose();
+ Assert.That(info.Disposed.Task.IsCompletedSuccessfully, Is.True);
+ }
+
+ [Test]
+ public void Handle_Equality()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var info1 = GetInfo(1).AsCompleted();
+ var info2 = GetInfo(2).AsCompleted();
+ var handle1a = global.Load(info1, AssetPriority.Synchronous);
+ var handle2 = global.Load(info2, AssetPriority.Synchronous);
+ var handle1b = global.Load(info1, AssetPriority.Synchronous);
+
+ Assert.That(handle1a, Is.EqualTo(handle1b));
+ Assert.That(handle1a, Is.Not.EqualTo(handle2));
+ Assert.That(handle2, Is.Not.EqualTo(handle1b));
+ Assert.That(handle1a.GetHashCode(), Is.EqualTo(handle1b.GetHashCode()));
+
+ Assert.That(handle1a == handle1b); // these are just for coverage
+ Assert.That(handle2 != handle1a);
+ Assert.That(handle1a.Equals((object)handle1b));
+ Assert.That(!handle1b.Equals(handle2));
+ }
+
+ [Test]
+ public void Handle_InvalidCopy_Duplicate()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var original = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var invalidCopy = original;
+ original.Dispose();
+
+ Assert.That(() => invalidCopy.Duplicate(), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Handle_InvalidCopy_Access()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var original = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var invalidCopy = original;
+ original.Dispose();
+
+ Assert.That(() => invalidCopy.Get(), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Handle_Default_Dispose()
+ {
+ AssetHandle handle = default;
+ Assert.That(() =>
+ {
+ handle.Dispose();
+ handle.Dispose();
+ }, Throws.Nothing);
+ }
+
+ [Test]
+ public void GenericHandle_FromAs()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var ghandle = thandle.As();
+
+ Assert.That(ghandle.Registry, Is.SameAs(thandle.Registry));
+ Assert.That(ghandle.AssetId, Is.EqualTo(thandle.AssetId));
+ thandle.Dispose();
+
+ var thandle2 = ghandle.As();
+ CommonAssetChecks(global, thandle2, 1);
+ }
+
+ [Test]
+ public void GenericHandle_FromAsAndDisposal()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var ghandle = thandle.As();
+
+ Assert.That(ghandle.Registry, Is.SameAs(thandle.Registry));
+ Assert.That(ghandle.AssetId, Is.EqualTo(thandle.AssetId));
+ thandle.Dispose();
+ ghandle.Dispose();
+
+ Assert.That(() =>
+ {
+ ghandle.As().Get();
+ }, Throws.Exception);
+ }
+
+ [Test]
+ public void GenericHandle_FromAsWasDisposed()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ thandle.Dispose();
+ var ghandle = thandle.As();
+
+ Assert.That(() =>
+ {
+ ghandle.As().Get();
+ }, Throws.Exception);
+ }
+
+ [Test]
+ public void GenericHandle_FromDuplicate()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var ghandle = thandle.AsDuplicate();
+
+ Assert.That(ghandle.Registry, Is.SameAs(thandle.Registry));
+ Assert.That(ghandle.AssetId, Is.EqualTo(thandle.AssetId));
+
+ var thandle2 = ghandle.As();
+ CommonAssetChecks(global, thandle2, 1);
+ CommonAssetChecks(global, thandle, 1);
+ }
+
+ [Test]
+ public void GenericHandle_TypeCheck()
+ {
+#if DEBUG
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+
+ var ghandle = thandle.AsDuplicate();
+ Assert.That(() =>
+ {
+ using var _ = ghandle.As();
+ }, Throws.Nothing);
+
+ ghandle = thandle.AsDuplicate();
+ Assert.That(() =>
+ {
+ using var _ = ghandle.As(); // no relation
+ }, Throws.InstanceOf());
+#else
+ Assert.Ignore("Type checks are only done in debug builds");
+ return;
+#endif
+ }
+
+ [Test]
+ public void GenericHandle_FromDuplicateAndDisposal()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var ghandle = thandle.AsDuplicate();
+
+ Assert.That(ghandle.Registry, Is.SameAs(thandle.Registry));
+ Assert.That(ghandle.AssetId, Is.EqualTo(thandle.AssetId));
+ thandle.Dispose();
+ ghandle.Dispose();
+
+ Assert.That(() =>
+ {
+ ghandle.As().Get();
+ }, Throws.Exception);
+ }
+
+ [Test]
+ public void GenericHandle_FromDuplicateWasDisposed()
+ {
+ using var global = new AssetRegistry(DI);
+ var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ thandle.Dispose();
+
+ Assert.That(() =>
+ {
+ var ghandle = thandle.AsDuplicate();
+ }, Throws.Exception);
+ }
+
+
+ [Test]
+ public void Apply_SyncAfter()
+ {
+ using var global = new AssetRegistry(DI);
+
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+
+ Assert.That(counter, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task Apply_HighBeforeLoadStart(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ Assert.That(counter, Is.Zero);
+ global.Update();
+ Assert.That(counter, Is.Zero);
+
+ info.FinishLoad.SetResult();
+ var asset = await handle.GetAsync(ct);
+
+ Assert.That(counter, Is.Zero); // apply has to be called on main thread - during Update
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1));
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1)); // but not twice
+ }
+
+ [Test]
+ public async Task Apply_HighDuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+ await info.StartedLoad.Task;
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ Assert.That(counter, Is.Zero);
+ global.Update();
+ Assert.That(counter, Is.Zero);
+
+ info.FinishLoad.SetResult();
+ var asset = await handle.GetAsync(ct);
+
+ Assert.That(counter, Is.Zero); // apply has to be called on main thread - during Update
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1));
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1)); // but not twice
+ }
+
+ [Test]
+ public async Task Apply_HighAfterLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+ using var handle = global.Load(info, AssetPriority.High);
+ var asset = await handle.GetAsync(ct);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ Assert.That(counter, Is.EqualTo(1)); // main thread and finished loading - fastpath
+ }
+
+ [Test]
+ public async Task Apply_LowBeforeStart(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+ using var handle = global.Load(info, AssetPriority.Low);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ Assert.That(counter, Is.Zero); // not even started
+
+ global.Update();
+ Assert.That(counter, Is.Zero); // just started, not applied
+
+ var asset = await handle.GetAsync(ct);
+ Assert.That(counter, Is.Zero); // finished but not applied
+
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task Apply_HighMultipleDuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task;
+ List events = [];
+ global.Apply(handle, _ => events.Add(1));
+ global.Apply(handle, _ => events.Add(2));
+ global.Apply(handle, _ => events.Add(3));
+ info.FinishLoad.SetResult();
+ var asset = await handle.GetAsync(ct);
+
+ global.Update();
+ Assert.That(events, Is.EqualTo([1, 2, 3]));
+ }
+
+ [Test]
+ public async Task Apply_HighDuringLoadFromAsync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task;
+ int counter = 0;
+ await Task.Run(() => global.Apply(handle, _ => counter++), ct);
+ info.FinishLoad.SetResult();
+ await handle.GetAsync(ct);
+
+ Assert.That(counter, Is.Zero); // was not finished
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task Apply_HighAfterLoadFromAsync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.High);
+ await handle.GetAsync(ct);
+
+ int counter = 0;
+ await Task.Run(() => global.Apply(handle, _ => counter++), ct);
+
+ Assert.That(counter, Is.Zero); // was finished but Apply was not on main thread
+ global.Update();
+ Assert.That(counter, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task Apply_MixedAsyncOrder(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Low);
+
+ List events = [];
+ global.Apply(handle, _ => events.Add(1));
+ await Task.Run(() => global.Apply(handle, _ => events.Add(2)), ct);
+ global.Apply(handle, _ => events.Add(3));
+ await Task.Run(() => global.Apply(handle, _ => events.Add(4)), ct);
+
+ global.Update();
+ await handle.GetAsync(ct);
+ global.Update();
+ Assert.That(events, Is.EqualTo([1, 2, 3, 4]));
+ }
+
+ [Test]
+ public void Apply_GlobalFromLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ int counter = 0;
+ local.Apply(handle, _ => counter++);
+ Assert.That(counter, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void Apply_DefaultHandle()
+ {
+ using var global = new AssetRegistry(DI);
+
+ Assert.That(() =>
+ {
+ global.Apply(default, _ => { });
+ }, Throws.ArgumentException);
+ }
+
+ [Test]
+ public void Apply_InvalidHandle()
+ {
+ using var global = new AssetRegistry(DI);
+
+ var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var invalidCopy = handle;
+ handle.Dispose();
+
+ Assert.That(() =>
+ {
+ global.Apply(invalidCopy, _ => { });
+ }, Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Apply_AfterRegistryDisposal()
+ {
+ var global = new AssetRegistry(DI);
+ var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ global.Dispose();
+
+ Assert.That(() =>
+ {
+ global.Apply(handle, _ => { });
+ }, Throws.InstanceOf());
+ }
+
+ [Test]
+ public async Task Apply_AfterAssetDisposal(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Low);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ global.Update();
+ await handle.GetAsync(ct);
+ handle.Dispose(); // now the queue contains one dead asset ID
+ global.Update();
+
+ Assert.That(counter, Is.Zero);
+ }
+
+ [Test]
+ public async Task Apply_AfterAssetRevival(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+ var handle = global.Load(info, AssetPriority.Low);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ global.Update();
+ await handle.GetAsync(ct);
+ handle.Dispose(); // now the queue contains one dead asset ID
+ Assert.That(info.Disposed.Task.IsCompletedSuccessfully); // and it really *is* dead
+ Assert.That(counter, Is.Zero);
+
+ handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ // now the queue contains the asset ID but not the asset the apply action was targeted at
+
+ global.Update();
+
+ Assert.That(counter, Is.Zero);
+ }
+
+ [Test]
+ public void Apply_ErrorSync()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+
+ Assert.That(() =>
+ {
+ global.Apply(handle, _ => throw new TestException());
+ }, Throws.InstanceOf());
+
+ _ = handle.Get(); // does not throw because Asset is still valid
+ }
+
+ [Test]
+ public async Task Apply_ErrorAsync(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info1 = GetInfo(1);
+ var info2 = GetInfo(2);
+ var info3 = GetInfo(3);
+ using var handle1 = global.Load(info1, AssetPriority.High);
+ using var handle2 = global.Load(info2, AssetPriority.High);
+ using var handle3 = global.Load(info3, AssetPriority.High);
+
+ int counter = 0;
+ global.Apply(handle1, _ => throw new TestException());
+ global.Apply(handle2, _ => counter++);
+ global.Apply(handle3, _ => throw new TestException());
+ global.Apply(handle1, _ => counter++); // subsequent apply actions are still executed
+
+ info1.FinishLoad.SetResult();
+ info2.FinishLoad.SetResult();
+ info3.FinishLoad.SetResult();
+ await Task.WhenAll([
+ handle1.GetAsync(ct).AsTask(),
+ handle2.GetAsync(ct).AsTask(),
+ handle3.GetAsync(ct).AsTask(),
+ ]);
+
+ Assert.That(() =>
+ {
+ global.Update();
+ }, Throws.InstanceOf());
+ Assert.That(counter, Is.EqualTo(2));
+ }
+
+ [Test]
+ public async Task Apply_ErrorDuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+ info.FinishLoad.SetException(new TestException());
+
+ await Assert.ThatAsync(async () =>
+ {
+ await handle.GetAsync(ct);
+ }, Throws.InstanceOf());
+
+ Assert.That(counter, Is.Zero);
+ global.Update();
+ Assert.That(counter, Is.Zero); // even after Update no apply action of erroneous asset is called
+ }
+
+ [Test]
+ public async Task Apply_ErrorAfterLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await Assert.ThatAsync(async () =>
+ {
+ await handle.GetAsync(ct);
+ }, Throws.InstanceOf());
+
+ int counter = 0;
+ global.Apply(handle, _ => counter++);
+
+ Assert.That(counter, Is.Zero);
+ global.Update();
+ Assert.That(counter, Is.Zero); // even after Update no apply action of erroneous asset is called
+ }
+
+ [Test]
+ public void TryGet_AfterLoad()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+
+ Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.True);
+ Assert.That(handle2.AssetId, Is.EqualTo(handle.AssetId));
+ Assert.That(handle2.Get(), Is.SameAs(handle.Get()));
+
+ handle.Dispose();
+ Assert.That(handle2.Get, Throws.Nothing);
+ Assert.That(handle2.Dispose, Throws.Nothing);
+ }
+
+ [Test]
+ public async Task TryGet_DuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1);
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task;
+ Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.True);
+ info.FinishLoad.SetResult();
+ var asset = await handle2.GetAsync(ct);
+ Assert.That(asset, Is.SameAs(handle.Asset));
+ Assert.That(asset, Is.Not.Null);
+ }
+
+ [Test]
+ public async Task TryGet_BeforeLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsCompleted();
+ using var handle = global.Load(info, AssetPriority.Low);
+
+ Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.True);
+ global.Update();
+ var asset = await handle2.GetAsync(ct);
+ Assert.That(asset, Is.SameAs(handle.Asset));
+ Assert.That(asset, Is.Not.Null);
+ }
+
+ [Test]
+ public async Task TryGet_ErrorDuringLoad(CancellationToken ct)
+ {
+ using var global = new AssetRegistry(DI);
+ var info = GetInfo(1).AsErroneous();
+ using var handle = global.Load(info, AssetPriority.High);
+
+ await info.StartedLoad.Task;
+ Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.True);
+ await Assert.ThatAsync(() => handle2.GetAsync(ct).AsTask(), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void TryGet_DefaultId()
+ {
+ using var global = new AssetRegistry(DI);
+ Assert.That(global.TryGet(default, out var handle), Is.False);
+ Assert.That(handle, Is.Default);
+ }
+
+ [Test]
+ public void TryGet_InvalidId()
+ {
+ using var global = new AssetRegistry(DI);
+ Assert.That(global.TryGet(Guid.NewGuid(), out var handle), Is.False);
+ Assert.That(handle, Is.Default);
+ }
+
+ [Test]
+ public void TryGet_WrongType()
+ {
+ using var global = new AssetRegistry(DI);
+ using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.False);
+ Assert.That(handle2, Is.Default);
+ }
+
+ [Test]
+ public void TryGet_DeadAsset()
+ {
+ using var global = new AssetRegistry(DI);
+ var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ handle.Dispose();
+ Assert.That(global.TryGet(handle.AssetId, out _), Is.False);
+ }
+
+ [Test]
+ public void TryGet_LocalFromLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ Assert.That(local.TryGet(handle.AssetId, out _), Is.True);
+ }
+
+ [Test]
+ public void TryGet_GlobalFromLocal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ Assert.That(local.TryGet(handle.AssetId, out _), Is.True);
+ }
+
+ [Test]
+ public void TryGet_LocalFromGlobal()
+ {
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+ using var handle = local.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ Assert.That(global.TryGet(handle.AssetId, out _), Is.False);
+ }
+
+ [Test]
+ public void Stats()
+ {
+ // Stats are not terribly important, especially in error cases I accept that stats will not be correct
+ // this one test should suffice for now
+ using var global = new AssetRegistry(DI);
+ using var local = new AssetRegistry(DI, global);
+
+ // one asset being loaded twice
+ using var h1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ using var h11 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+
+ // one asset being removed
+ var h2 = global.Load(GetInfo(2).AsCompleted(), AssetPriority.Synchronous);
+ h2.Dispose();
+
+ // one asset being created twice
+ var h3 = global.Load(GetInfo(3).AsCompleted(), AssetPriority.Synchronous);
+ h3.Dispose();
+ h3 = global.Load(GetInfo(3).AsCompleted(), AssetPriority.Synchronous);
+ h3.Dispose();
+
+ // two assets being created but not loaded
+ var h4 = global.Load(GetInfo(4), AssetPriority.High);
+ var h5 = global.Load(GetInfo(5), AssetPriority.Low);
+
+ // additional assets in the local registry
+ using var h6 = local.Load(GetInfo(6).AsCompleted(), AssetPriority.Synchronous);
+ var h7 = local.Load(GetInfo(7).AsCompleted(), AssetPriority.Synchronous);
+ h7.Dispose();
+
+ Assert.That(global.Stats, Is.EqualTo(new AssetRegistryStats(
+ created: 6,
+ loaded: 4,
+ removed: 3,
+ total: 3
+ )));
+
+ Assert.That(local.Stats, Is.EqualTo(new AssetRegistryStats(
+ created: 6 + 2,
+ loaded: 4 + 2,
+ removed: 3 + 1,
+ total: 3 + 1
+ )));
+
+ // just to fill code coverage
+ Assert.That(() =>
+ {
+ var stats = global.Stats;
+ var a = stats - stats;
+ _ = stats.Created;
+ _ = stats.Loaded;
+ _ = stats.Removed;
+ _ = stats.Total;
+ _ = stats.ToString();
+ }, Throws.Nothing);
+ }
+
+ [Test]
+ public void Delayed_Undelayed()
+ {
+ using var global = new AssetRegistryDelayed(new AssetRegistry(DI));
+
+ var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var handle2 = handle1; // we use an invalid copy to check for asset disposal
+ handle1.Dispose();
+
+ Assert.That(handle2.Get, Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Delayed_Delayed()
+ {
+ using var global = new AssetRegistryDelayed(new AssetRegistry(DI));
+ global.DelayDisposals = true;
+
+ var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var handle2 = handle1; // we use an invalid copy to check for asset disposal
+ handle1.Dispose();
+
+ Assert.That(handle2.Get, Throws.Nothing);
+
+ global.DelayDisposals = false;
+ Assert.That(handle2.Get, Throws.Nothing);
+ }
+
+ [Test]
+ public void Delayed_DelayedTwice()
+ {
+ using var global = new AssetRegistryDelayed(new AssetRegistry(DI));
+ global.DelayDisposals = true;
+
+ var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ var handle2 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous);
+ handle1.Dispose();
+
+ Assert.That(handle2.Get, Throws.Nothing);
+
+ global.DelayDisposals = false;
+ Assert.That(handle2.Get, Throws.Nothing);
+
+ global.DelayDisposals = true;
+ global.DelayDisposals = false;
+ Assert.That(handle2.Get, Throws.Nothing); // still alive
+ }
+}
diff --git a/zzre.core.tests/zzre.core.tests.csproj b/zzre.core.tests/zzre.core.tests.csproj
index 4eab2a12..494cbd49 100644
--- a/zzre.core.tests/zzre.core.tests.csproj
+++ b/zzre.core.tests/zzre.core.tests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/zzre.core/Diagnostic.cs b/zzre.core/Diagnostic.cs
new file mode 100644
index 00000000..91a74f1e
--- /dev/null
+++ b/zzre.core/Diagnostic.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Serilog;
+
+namespace zzre;
+
+public enum DiagnosticSeverity
+{
+ Info,
+ Warning,
+ Error,
+ InternalError
+}
+
+public class DiagnosticCategory
+{
+ private readonly List types = [];
+
+ public string Name { get; }
+ public IReadOnlyList Types => types;
+
+ public DiagnosticCategory(string name) => Name = name;
+
+ public DiagnosticType Error(string message, string? footNote = null, params string[] sourceInfoMessages) =>
+ Create(DiagnosticSeverity.Error, message, footNote, sourceInfoMessages);
+ public DiagnosticType Warning(string message, string? footNote = null, params string[] sourceInfoMessages) =>
+ Create(DiagnosticSeverity.Warning, message, footNote, sourceInfoMessages);
+ public DiagnosticType Information(string message, string? footNote = null, params string[] sourceInfoMessages) =>
+ Create(DiagnosticSeverity.Info, message, footNote, sourceInfoMessages);
+ public DiagnosticType Create(DiagnosticSeverity severity, string message, string? footNote = null, params string[] sourceInfoMessages)
+ {
+ var type = new DiagnosticType(this, types.Count + 1, severity, message, sourceInfoMessages, footNote);
+ types.Add(type);
+ return type;
+ }
+
+ internal DiagnosticType CreateWithFootNote(DiagnosticSeverity severity, string message, string footNote, params string[] sourceInfoMessages)
+ {
+ var type = new DiagnosticType(this, types.Count + 1, severity, message, sourceInfoMessages, footNote);
+ types.Add(type);
+ return type;
+ }
+}
+
+public class DiagnosticType
+{
+ public DiagnosticCategory Category { get; }
+ public int CodeNumber { get; }
+ public DiagnosticSeverity Severity { get; }
+ public string Message { get; }
+ public IReadOnlyList SourceInfoMessages { get; }
+ public string? FootNote { get; }
+ public string Code => $"{Category.Name}{CodeNumber:D3}";
+
+ public DiagnosticType(
+ DiagnosticCategory category,
+ int codeNumber,
+ DiagnosticSeverity severity,
+ string message,
+ IReadOnlyList sourceInfoMessages,
+ string? footNote)
+ {
+ Category = category;
+ CodeNumber = codeNumber;
+ Severity = severity;
+ Message = message;
+ SourceInfoMessages = sourceInfoMessages;
+ FootNote = footNote;
+ }
+
+ public Diagnostic Create(string[]? messageParams = null, DiagnosticLocation[]? sourceInfos = null) =>
+ new(this, messageParams ?? [], sourceInfos ?? []);
+}
+
+public readonly record struct DiagnosticLocation(
+ string Resource,
+ int? LineStart = null,
+ int? LineEnd = null,
+ int? ColumnStart = null,
+ int? ColumnEnd = null) : IComparable
+{
+ public int CompareTo(DiagnosticLocation other)
+ {
+ var result = Resource.CompareTo(other.Resource);
+ if (result != 0)
+ return result;
+
+ var myLineStart = LineStart ?? -1;
+ var otherLineStart = other.LineStart ?? -1;
+ if (myLineStart != otherLineStart)
+ return myLineStart - otherLineStart;
+
+ var myColumn = ColumnStart ?? -1;
+ var otherColumn = other.ColumnStart ?? -1;
+ if (myColumn != otherColumn)
+ return myColumn - otherColumn;
+ return 0;
+ }
+
+ public readonly override string ToString()
+ {
+ var builder = new StringBuilder();
+ WriteTo(builder);
+ return builder.ToString();
+ }
+
+ public readonly void WriteTo(StringBuilder builder)
+ {
+ builder.Append(Resource);
+ if (LineStart is null)
+ return;
+ builder.Append($"({LineStart}");
+ if (ColumnStart is not null)
+ builder.Append($":{ColumnStart}");
+ if (LineEnd is not null)
+ {
+ builder.Append($"->{LineEnd}");
+ if (ColumnEnd is not null)
+ builder.Append($":{ColumnEnd}");
+ }
+ builder.Append(')');
+ }
+
+ public static bool operator <(DiagnosticLocation left, DiagnosticLocation right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator <=(DiagnosticLocation left, DiagnosticLocation right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ public static bool operator >(DiagnosticLocation left, DiagnosticLocation right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(DiagnosticLocation left, DiagnosticLocation right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+}
+
+public readonly record struct Diagnostic(
+ DiagnosticType Type,
+ IReadOnlyList MessageParams,
+ IReadOnlyList SourceInfos)
+ : IComparable
+{
+ public DiagnosticCategory Category => Type.Category;
+ public DiagnosticSeverity Severity => Type.Severity;
+ public string FormattedMessage => string.Format(Type.Message, MessageParams.ToArray());
+ public string? FormattedFootNote => Type.FootNote == null ? null : string.Format(Type.FootNote, MessageParams.ToArray());
+
+ public void Write(ILogger logger)
+ {
+ var sourceInfo = SourceInfos.Any() ? SourceInfos.First().ToString() + ": " : "";
+ logger.Write(SeverityToSerilog(Type.Severity), sourceInfo + Type.Message, MessageParams);
+ if (Type.FootNote is not null)
+ logger.Information(sourceInfo + Type.Message, MessageParams);
+ }
+
+ public void WriteToConsole()
+ {
+ var prevBackground = Console.BackgroundColor;
+ Console.BackgroundColor = SeverityToConsoleColor(Type.Severity);
+ Console.Write(Type.Severity switch
+ {
+ DiagnosticSeverity.Info => "INFO ",
+ DiagnosticSeverity.Warning => "WARN ",
+ DiagnosticSeverity.Error => "ERROR ",
+ DiagnosticSeverity.InternalError => "INTERR",
+ _ => "????? "
+ });
+ Console.BackgroundColor = prevBackground;
+
+ if (SourceInfos.Any())
+ {
+ Console.Write(SourceInfos.First());
+ Console.Write(": ");
+ }
+ Console.WriteLine(FormattedMessage);
+
+ if (Type.FootNote is not null)
+ {
+ Console.Write("NOTE ");
+ Console.WriteLine(FormattedFootNote);
+ }
+ }
+
+ private static Serilog.Events.LogEventLevel SeverityToSerilog(DiagnosticSeverity s) => s switch
+ {
+ DiagnosticSeverity.Info => Serilog.Events.LogEventLevel.Information,
+ DiagnosticSeverity.Warning => Serilog.Events.LogEventLevel.Warning,
+ DiagnosticSeverity.Error => Serilog.Events.LogEventLevel.Error,
+ DiagnosticSeverity.InternalError => Serilog.Events.LogEventLevel.Fatal,
+ _ => throw new NotImplementedException($"Unimplemented severity: {s}")
+ };
+
+ private static ConsoleColor SeverityToConsoleColor(DiagnosticSeverity s) => s switch
+ {
+ DiagnosticSeverity.Info => ConsoleColor.DarkGray,
+ DiagnosticSeverity.Warning => ConsoleColor.DarkYellow,
+ DiagnosticSeverity.Error => ConsoleColor.DarkRed,
+ DiagnosticSeverity.InternalError => ConsoleColor.DarkMagenta,
+ _ => throw new NotImplementedException($"Unimplemented severity: {s}")
+ };
+
+ public int CompareTo(Diagnostic other)
+ {
+ var aLoc = SourceInfos.Any() ? SourceInfos.First() : null as DiagnosticLocation?;
+ var bLoc = other.SourceInfos.Any() ? other.SourceInfos.First() : null as DiagnosticLocation?;
+ if (aLoc is null && bLoc is not null)
+ return 1;
+ if (aLoc is not null && bLoc is null)
+ return -1;
+ if (aLoc is not null && bLoc is not null)
+ {
+ foreach (var (a, b) in SourceInfos.Zip(other.SourceInfos))
+ {
+ var locResult = a.CompareTo(b);
+ if (locResult != 0)
+ return locResult;
+ }
+ }
+ var typeResult = Type.Code.CompareTo(other.Type.Code);
+ if (typeResult != 0)
+ return typeResult;
+ return -1;
+ }
+
+ public static bool operator <(Diagnostic left, Diagnostic right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator <=(Diagnostic left, Diagnostic right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ public static bool operator >(Diagnostic left, Diagnostic right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(Diagnostic left, Diagnostic right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+}
\ No newline at end of file
diff --git a/zzre.core/NullDisposable.cs b/zzre.core/NullDisposable.cs
new file mode 100644
index 00000000..10a5859d
--- /dev/null
+++ b/zzre.core/NullDisposable.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace zzre;
+
+public sealed class NullDisposable : IDisposable
+{
+ private NullDisposable() { }
+ public static readonly NullDisposable Instance = new();
+ public void Dispose() { }
+}
diff --git a/zzre.core/assetregistry/Asset.cs b/zzre.core/assetregistry/Asset.cs
deleted file mode 100644
index a42053f1..00000000
--- a/zzre.core/assetregistry/Asset.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace zzre;
-
-/// The loading state of an asset
-public enum AssetState
-{
- /// Loading of the asset has not been started yet
- /// E.g. Low prioritised asset loading is started across several frames
- Queued,
- /// The asset is currently loading
- Loading,
- /// The primary asset is loaded, but needs to wait for secondary asset loading to finish
- LoadingSecondary,
- /// The asset has been completly loaded and is ready to use
- /// This state does not entail applying the asset
- Loaded,
- /// The asset has been disposed of and should not be used anymore
- Disposed,
- /// There was some error during loading and the asset should not be used
- Error
-}
-
-/// The internal interface into an asset
-internal interface IAsset : IDisposable
-{
- Guid ID { get; }
- AssetState State { get; }
- /// The task communicates completion state and error handling during loading
- Task LoadTask { get; }
- /// The priority that the asset was *effectively* loaded with
- AssetLoadPriority Priority { get; set; }
- /// The current reference count to be atomically modified to keep assets alive or disposing them
- /// Modifying has to be done using the and methods
- int RefCount { get; }
- /// The *stored* apply actions to be taken after loading
- /// This will not include immediate apply actions if the asset is loaded synchronously or was already loaded
- OnceAction ApplyAction { get; }
-
- /// Starts the loading of the asset on the thread pool
- /// This call is ignored if the is not
- void StartLoading();
- /// Synchronously completes loading of the asset
- /// This call will also rethrow loading exceptions
- void Complete();
- /// Atomically increases the reference count
- void AddRef();
- /// Atomically decreases the reference count and disposes the asset if necessary
- /// It will also signal a disposal to the registry
- void DelRef();
- /// Rethrows a loading exception that already occured
- /// Assumes that the load task has already completed with an error
- void ThrowIfError();
-}
-
-/// The base class for asset types
-public abstract class Asset : IAsset
-{
- protected static ValueTask> NoSecondaryAssets =>
- ValueTask.FromResult(Enumerable.Empty());
-
- /// The of the apparent registry to be used during loading
- protected readonly ITagContainer diContainer;
- private readonly TaskCompletionSource completionSource = new();
- private string? description;
- private AssetHandle[] secondaryAssets = [];
- private int refCount;
-
- private IAssetRegistryInternal InternalRegistry { get; }
- public IAssetRegistry Registry { get; }
- /// An unique identifier given by the registry related to the Info value in order to efficiently address an asset instance
- /// The ID is chosen randomly and will change at least per process per asset
- public Guid ID { get; }
- /// The current loading state of the asset
- public AssetState State { get; private set; }
- Task IAsset.LoadTask => completionSource.Task;
- int IAsset.RefCount => refCount;
- AssetLoadPriority IAsset.Priority { get; set; }
- OnceAction IAsset.ApplyAction { get; } = new();
-
- /// Constructs the base information for an asset
- /// Only call this constructor with data given by a registry
- /// The apparent registry of this asset to report and load secondary assets from
- /// The ID chosen by the registry for this asset
- public Asset(IAssetRegistry registry, Guid id)
- {
- Registry = registry;
- InternalRegistry = registry.InternalRegistry;
- diContainer = registry.DIContainer;
- ID = id;
- }
-
- void IDisposable.Dispose()
- {
- if (State != AssetState.Error)
- State = AssetState.Disposed;
-
- Unload();
-
- foreach (var handle in secondaryAssets)
- handle.Dispose();
- secondaryAssets = [];
- }
-
- void IAsset.StartLoading()
- {
- lock (this)
- {
- if (State != AssetState.Queued)
- return;
- State = AssetState.Loading;
- Task.Run(PrivateLoad, InternalRegistry.Cancellation);
- }
- }
-
- void IAsset.Complete()
- {
- lock (this)
- {
- switch (State)
- {
- case AssetState.Loaded: return;
-
- case AssetState.Loading:
- case AssetState.LoadingSecondary:
- completionSource.Task.WaitAndRethrow();
- return;
-
- case AssetState.Queued:
- State = AssetState.Loading;
- PrivateLoad().WaitAndRethrow();
- return;
-
- case AssetState.Disposed:
- throw new ObjectDisposedException(ToString());
- case AssetState.Error:
- (this as IAsset).ThrowIfError();
- return;
-
- default:
- throw new NotImplementedException($"Unimplemented asset state {State}");
- }
- }
- }
-
- void IAsset.AddRef() => Interlocked.Increment(ref refCount);
- void IAsset.DelRef()
- {
- int oldRefCount;
- while (true)
- {
- oldRefCount = refCount;
- if (oldRefCount <= 0)
- return;
- if (Interlocked.CompareExchange(ref refCount, oldRefCount - 1, oldRefCount) == oldRefCount)
- break;
- }
- if (oldRefCount == 1) // we just hit zero
- {
- lock (this)
- {
- (this as IAsset).Dispose();
- InternalRegistry.QueueRemoveAsset(this).AsTask().WaitAndRethrow();
- }
- }
- }
-
- private async Task PrivateLoad()
- {
- if (State != AssetState.Loading)
- throw new InvalidOperationException("Asset.PrivateLoad was called during an unexpected state");
-
- var ct = InternalRegistry.Cancellation;
- try
- {
- var secondaryAssetSet = await Load();
- secondaryAssets = secondaryAssetSet.ToArray();
- EnsureLocality(secondaryAssets);
- ct.ThrowIfCancellationRequested();
-
- if (secondaryAssets.Length > 0 && NeedsSecondaryAssets)
- {
- lock (this)
- {
- State = AssetState.LoadingSecondary;
- }
- await InternalRegistry.WaitAsyncAll(secondaryAssets);
- }
-
- ct.ThrowIfCancellationRequested();
- State = AssetState.Loaded;
- completionSource.SetResult();
- await InternalRegistry.QueueApplyAsset(this);
- }
- catch (Exception ex)
- {
- lock(this)
- {
- State = AssetState.Error;
- completionSource.SetException(ex);
- (this as IDisposable).Dispose();
- }
- }
- }
-
- void IAsset.ThrowIfError()
- {
- if (State == AssetState.Error)
- {
- completionSource.Task.WaitAndRethrow();
- throw new InvalidOperationException("Asset was marked erroneous but does not contain exception");
- }
- }
-
- [Conditional("DEBUG")]
- private void EnsureLocality(AssetHandle[] secondaryAssets)
- {
- foreach (var secondary in secondaryAssets)
- if (!InternalRegistry.IsLocalRegistry && secondary.registryInternal.IsLocalRegistry)
- throw new InvalidOperationException("Global assets cannot load local assets as secondary ones");
- }
-
- /// Whether marking this asset as loaded should be deferred until all secondary asset are loaded as well
- protected virtual bool NeedsSecondaryAssets { get; } = true;
- /// Override this method to actually load the asset contents
- /// This method can be called asynchronously
- /// The set of secondary assets to be loaded from the same registry interface as this asset
- protected abstract ValueTask> Load();
- /// Unloads any resources the asset might hold
- /// It is not necessary to manually dispose secondary asset handles
- protected abstract void Unload();
-
- /// Produces a description of the asset to be shown in debug logs and tools
- /// A description of the asset instance for debugging
- public override sealed string ToString() => description ??= ToStringInner();
-
- /// Produces a description of the asset to be shown in debug logs and tools
- /// Used to cache description strings
- /// A description of the asset instance for debugging
- protected virtual string ToStringInner() => $"{GetType().Name} {ID}";
-}
diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs
index 09c6712e..404ac7be 100644
--- a/zzre.core/assetregistry/AssetHandle.cs
+++ b/zzre.core/assetregistry/AssetHandle.cs
@@ -1,162 +1,89 @@
-using System;
+using System;
using System.Diagnostics;
-
namespace zzre;
-/// An untyped handle to an asset
-/// Keeping a handle to the asset keeps the asset alive
-public struct AssetHandle : IDisposable, IEquatable
+public struct AssetHandle(IAssetRegistry registry, Guid assetId) : IAssetHandle, IEquatable
{
- /// An invalid handle that is not tied to any registry nor asset
- /// Only disposing this handle is an allowed action
- public static readonly AssetHandle Invalid = new(registry: null!, Guid.Empty) { wasDisposed = true };
-
- internal readonly IAssetRegistryInternal registryInternal;
- private readonly AssetHandleScope? handleScope;
private bool wasDisposed;
+ public readonly IAssetRegistry Registry => registry;
+ public readonly Guid AssetId => assetId;
- /// The the asset was loaded at
- public readonly IAssetRegistry Registry => registryInternal;
- /// The unique ID of the asset
- public readonly Guid AssetID { get; }
- /// Checks whether this asset is marked as
- public readonly bool IsLoaded
- {
- get
- {
- CheckDisposed();
- return registryInternal.IsLoaded(AssetID);
- }
- }
-
- internal AssetHandle(IAssetRegistry registry, AssetHandleScope handleScope, Guid assetId)
+ internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed) : this(registry, assetId)
{
- this.handleScope = handleScope;
- this.registryInternal = registry as IAssetRegistryInternal ??
- throw new ArgumentException("Cannot create asset handles from registry decorators", nameof(registry));
- AssetID = assetId;
+ this.wasDisposed = wasDisposed;
}
- internal AssetHandle(AssetHandleScope handleScope, Guid assetId)
+ public void Dispose()
{
- this.handleScope = handleScope;
- registryInternal = (handleScope as IAssetRegistry).InternalRegistry;
- AssetID = assetId;
+ if (wasDisposed) return;
+ wasDisposed = true;
+ if (Registry is not null && AssetId != default)
+ ((IAssetRegistryInternal)Registry).DelRef(AssetId);
}
- internal AssetHandle(AssetRegistry registry, Guid assetId)
+ public readonly TAsset Get()
+ where TAsset : class, IAsset
{
- registryInternal = registry;
- AssetID = assetId;
+ TypeCheck(typeof(TAsset));
+ var tmp = new AssetHandle(registry, assetId, wasDisposed); // an invalid copy
+ return tmp.Get();
}
- /// Disposes the stake on the asset this handle is tied to
- /// *May* trigger disposal of the asset and related secondary assets
- public void Dispose()
+ public AssetHandle As()
+ where TAsset : class, IAsset
{
- if (wasDisposed)
- return;
+ TypeCheck(typeof(TAsset));
+ AssetHandle result = new(registry, assetId, wasDisposed);
wasDisposed = true;
- if (handleScope is null)
- registryInternal?.DisposeHandle(this);
- else
- handleScope.DisposeHandle(this);
+ return result;
}
- [Conditional("DEBUG")]
- private readonly void CheckDisposed() =>
- ObjectDisposedException.ThrowIf(wasDisposed || AssetID == Guid.Empty, this);
-
- /// Returns a loaded asset instance
- /// The asset has to be marked as , otherwise it will try to synchronously wait for loading completion
- /// The actual type of the asset instance
- /// The asset instance
- public readonly TValue Get() where TValue : Asset
+ public readonly AssetHandle AsDuplicate()
+ where TAsset : class, IAsset
{
- CheckDisposed();
- return registryInternal.GetLoadedAsset(AssetID);
+ ThrowIfDisposed();
+ TypeCheck(typeof(TAsset));
+ ((IAssetRegistryInternal)Registry).AddRef(AssetId);
+ return new(Registry, AssetId, false);
}
- /// Returns this handle as a typed asset handle
- /// This method does not check the actual type of the asset and will always succeed (given the handle was not disposed)
- /// The asset type to be used
- /// The typed asset handle
- public readonly AssetHandle As() where TValue : Asset
+ public AssetHandle Move()
{
- CheckDisposed();
- return (AssetHandle)this;
+ var result = this;
+ wasDisposed = true;
+ return result;
}
- /// Adds an apply action to the asset
- /// Depending on whether the asset is already loaded the action will be called immediately or only stored for later execution
- /// The type of the apply context given to the apply action
- /// The function pointer to call as apply action
- /// The apply context given to the apply action
- public unsafe readonly void Apply(
- delegate* managed applyFnptr,
- in TApplyContext applyContext)
+ public readonly AssetHandle Duplicate()
{
- CheckDisposed();
- registryInternal.AddApplyAction(this, applyFnptr, in applyContext);
+ ThrowIfDisposed();
+ ((IAssetRegistryInternal)Registry).AddRef(AssetId);
+ return new(Registry, AssetId, false);
}
- /// Adds an apply action to the asset
- /// Depending on whether the asset is already loaded the action will be called immediately or only stored for later execution
- /// The type of the apply context given to the apply action
- /// The delegate to call as apply action
- /// The apply context given to the apply action
- public readonly void Apply(
- IAssetRegistry.ApplyWithContextAction applyAction,
- in TApplyContext applyContext)
+ private readonly void ThrowIfDisposed()
{
- CheckDisposed();
- registryInternal.AddApplyAction(this, applyAction, in applyContext);
+ ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle));
+ ObjectDisposedException.ThrowIf(AssetId == Guid.Empty, typeof(AssetHandle));
+ ObjectDisposedException.ThrowIf(Registry?.WasDisposed is null or true, typeof(IAssetRegistry));
}
- /// Adds an apply action to the asset
- /// Depending on whether the asset is already loaded the action will be called immediately or only stored for later execution
- /// The delegate to call as apply action
- public readonly void Apply(Action applyAction)
+ [Conditional("DEBUG")]
+ private readonly void TypeCheck(Type type)
{
- CheckDisposed();
- registryInternal.AddApplyAction(this, applyAction);
+ if (wasDisposed || Registry?.WasDisposed is null or true || AssetId == Guid.Empty)
+ return;
+ ((IAssetRegistryInternal)Registry).CheckType(AssetId, type);
}
- public readonly override string ToString() => $"AssetHandle {AssetID}";
-
- public override readonly bool Equals(object? obj) => obj is AssetHandle handle && Equals(handle);
- public readonly bool Equals(AssetHandle other) => AssetID.Equals(other.AssetID);
- public override readonly int GetHashCode() => HashCode.Combine(AssetID);
- public static bool operator ==(AssetHandle left, AssetHandle right) => left.Equals(right);
- public static bool operator !=(AssetHandle left, AssetHandle right) => !(left == right);
-}
-
-/// A typed asset handle for convenience
-/// The actual type is only checked upon retrieval of the instance
-/// The type of the asset instance
-public struct AssetHandle : IDisposable, IEquatable>, IEquatable
- where TValue : Asset
-{
- ///
- public static readonly AssetHandle Invalid = new() { Inner = AssetHandle.Invalid };
-
- /// The untyped
- public AssetHandle Inner { get; private init; }
-
- public static explicit operator AssetHandle(AssetHandle handle) => new() { Inner = handle };
- public static implicit operator AssetHandle(AssetHandle handle) => handle.Inner;
-
- ///
- public void Dispose() => Inner.Dispose();
- ///
- public readonly TValue Get() => Inner.Get();
-
- public readonly override string ToString() => $"AssetHandle<{typeof(TValue).Name}> {Inner.AssetID}";
-
- public static bool operator ==(AssetHandle left, AssetHandle right) => left.Equals(right);
- public static bool operator !=(AssetHandle left, AssetHandle right) => !(left == right);
- public override readonly bool Equals(object? obj) => obj is AssetHandle handle && Equals(handle);
- public readonly bool Equals(AssetHandle other) => Inner.AssetID.Equals(other.Inner.AssetID);
- public readonly bool Equals(AssetHandle other) => Inner.AssetID.Equals(other.AssetID);
- public override readonly int GetHashCode() => HashCode.Combine(Inner);
+ public readonly override int GetHashCode() =>
+ HashCode.Combine(Registry, AssetId);
+ public readonly override bool Equals(object? obj) =>
+ obj is AssetHandle other && Equals(other);
+ public readonly bool Equals(AssetHandle other) =>
+ Registry == other.Registry && AssetId == other.AssetId;
+ public static bool operator ==(AssetHandle a, AssetHandle b) =>
+ a.Equals(b);
+ public static bool operator !=(AssetHandle a, AssetHandle b) =>
+ !a.Equals(b);
}
diff --git a/zzre.core/assetregistry/AssetHandleScope.cs b/zzre.core/assetregistry/AssetHandleScope.cs
deleted file mode 100644
index 69150bba..00000000
--- a/zzre.core/assetregistry/AssetHandleScope.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace zzre;
-
-/// An asset handle scope can be used to delay disposal of asset handles until a better point in time
-/// The registry assets are loaded and disposed at
-public sealed class AssetHandleScope(IAssetRegistry registry) : IAssetRegistry
-{
- // we use a dictionary to keep handles to the same asset from piling up
- // wasting memory and cycles at disposal time
- private readonly Dictionary handlesToDispose = new(128);
- private bool delayDisposals;
-
- IAssetRegistryInternal IAssetRegistry.InternalRegistry => Registry.InternalRegistry;
- /// The registry the handle scope uses for loading and disposal
- public IAssetRegistry Registry => registry;
- /// The of the underlying registry
- public AssetRegistryStats Stats => registry.Stats;
- /// The of the underlying registry
- public ITagContainer DIContainer => Registry.DIContainer;
-
- /// Whether disposal of handles returned by this are executed
- /// Setting this property to false will trigger all outstanding disposals
- public bool DelayDisposals
- {
- get => delayDisposals;
- set
- {
- delayDisposals = value;
- if (!value)
- {
- foreach (var (assetId, registryInternal) in handlesToDispose)
- registryInternal.DisposeHandle(new(registryInternal, this, assetId));
- handlesToDispose.Clear();
- }
- }
- }
-
- ///
- public unsafe AssetHandle Load(
- in TInfo info,
- AssetLoadPriority priority,
- delegate* managed applyFnptr,
- in TApplyContext applyContext)
- where TInfo : IEquatable
- {
- var handle = registry.Load(info, priority, applyFnptr, applyContext);
- return new(this, handle.AssetID);
- }
-
- ///
- public AssetHandle Load(
- in TInfo info,
- AssetLoadPriority priority,
- Action? applyAction = null)
- where TInfo : IEquatable
- {
- var handle = registry.Load(info, priority, applyAction);
- return new(this, handle.AssetID);
- }
-
- internal void DisposeHandle(AssetHandle handle)
- {
- if (!DelayDisposals ||
- !handlesToDispose.TryAdd(handle.AssetID, handle.registryInternal))
- handle.registryInternal.DisposeHandle(handle);
- }
-
- /// No-op as the underlying registry is supposed to apply the assets itself
- public void ApplyAssets() { } // it is just a scope
-
- ///
- public void Dispose()
- {
- DelayDisposals = false;
- }
-}
diff --git a/zzre.core/assetregistry/AssetHandleT.cs b/zzre.core/assetregistry/AssetHandleT.cs
new file mode 100644
index 00000000..32c0eb2b
--- /dev/null
+++ b/zzre.core/assetregistry/AssetHandleT.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using DotNext;
+
+namespace zzre;
+
+public interface IAssetHandle : IDisposable
+{
+ IAssetRegistry Registry { get; }
+ Guid AssetId { get; }
+}
+
+public struct AssetHandle(IAssetRegistry registry, Guid assetId) : IAssetHandle, IEquatable>
+ where TAsset : class, IAsset
+{
+ private bool wasDisposed;
+ public readonly IAssetRegistry Registry => registry;
+ public readonly Guid AssetId => assetId;
+
+ internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed) : this(registry, assetId)
+ {
+ this.wasDisposed = wasDisposed;
+ }
+
+ public void Dispose()
+ {
+ if (wasDisposed) return;
+ wasDisposed = true;
+ if (Registry is not null && AssetId != default)
+ ((IAssetRegistryInternal)Registry).DelRef(AssetId);
+ }
+
+ public readonly TAsset? Asset
+ {
+ get
+ {
+ ThrowIfDisposed();
+ var result = ((IAssetRegistryInternal)Registry).GetAsset(AssetId).Value;
+ return result?.IsSuccessful is true
+ ? (TAsset)result.Value.Value
+ : null;
+ }
+ }
+
+ public readonly TAsset Get()
+ {
+ ThrowIfDisposed();
+ if (!Registry.IsMainThread)
+ throw new InvalidOperationException("Synchronous asset loading is only allowed on the main thread");
+ var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId);
+ if (!lazy.IsValueCreated)
+ {
+ try
+ {
+ lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation);
+ }
+ catch (AggregateException e)
+ {
+ throw e.InnerException ?? e;
+ }
+ }
+ return (TAsset)lazy.Value!.Value; // throws on error
+ }
+
+ public readonly ValueTask