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 GetAsync(CancellationToken ct) + { + ThrowIfDisposed(); + var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); + if (lazy.Value is not Result result) + return new(DoGetAsync(ct)); + else if (result.IsSuccessful) + return ValueTask.FromResult((TAsset)result.Value); + else + return ValueTask.FromException(result.Error); + } + + private readonly async Task DoGetAsync(CancellationToken ct) + { + var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); + return (TAsset)await lazy.WithCancellation(ct); + } + + public AssetHandle As() + { + AssetHandle result = new(registry, assetId, wasDisposed); + wasDisposed = true; + return result; + } + + public AssetHandle Move() + { + var result = this; + wasDisposed = true; + return result; + } + + public readonly AssetHandle Duplicate() + { + ThrowIfDisposed(); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false); + } + + public readonly AssetHandle AsDuplicate() + { + ThrowIfDisposed(); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false); + } + + private readonly void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(AssetId == Guid.Empty, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(Registry?.WasDisposed is null or true, typeof(IAssetRegistry)); + } + + 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/AssetInfoRegistry.cs b/zzre.core/assetregistry/AssetInfoRegistry.cs deleted file mode 100644 index 37a846b4..00000000 --- a/zzre.core/assetregistry/AssetInfoRegistry.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace zzre; - -/// The locality of an asset type -/// This will determine whether an asset is loaded at a global or a local registry -public enum AssetLocality -{ - /// Global assets will be loaded at a global registry and shared across all users - Global, - /// Context assets will be loaded at a local registry and shared across users of the local registry - Context, - /// SingleUsage assets will be loaded at a local registry and never shared - SingleUsage, -} - -/// The application-wide registry of asset types and mapping of info values to asset IDs -/// The type of info values denoting a specific asset type -public static class AssetInfoRegistry where TInfo : IEquatable -{ - /// Constructs an asset instance - /// The registry the asset is registered at - /// The ID of the asset - /// The info value given to the Load method - /// A new instance of this asset type - public delegate Asset AssetConstructor(IAssetRegistry registry, Guid assetId, in TInfo info); - - private static readonly object @lock = new(); - private static readonly Dictionary infoToGuid = []; - private static readonly Dictionary guidToInfo = []; - private static AssetConstructor? constructor; - internal static string Name { get; private set; } = typeof(TInfo).FullName ?? "Unknown"; - internal static AssetLocality Locality { get; private set; } - - /// Registers this asset type with a manual constructor delegate - /// Asset types can only be registered once and have to be registered before attempting to load assets - /// The debug name of this asset type - /// The delegate constructing asset instances - /// The locality of this asset type - public static void Register(string name, AssetConstructor constructor, AssetLocality locality) - { - if (AssetInfoRegistry.constructor != null) - throw new InvalidOperationException($"Asset type with info {typeof(TInfo).FullName} was already registered"); - AssetInfoRegistry.Locality = locality; - AssetInfoRegistry.Name = name; - AssetInfoRegistry.constructor = constructor; - } - - /// Registers this asset type with a reflected class inheriting from - /// Asset types can only be registered once and have to be registered before attempting to load assets - /// A class inheriting from with a public constructor (, , ) - /// The locality of this asset type - public static void Register - <[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAsset> - (AssetLocality locality) where TAsset : Asset - { - var ctorInfo = - typeof(TAsset).GetConstructor([typeof(IAssetRegistry), typeof(Guid), typeof(TInfo)]) - ?? throw new ArgumentException("Could not find standard constructor", nameof(TAsset)); - Register(typeof(TAsset).Name, (IAssetRegistry registry, Guid guid, in TInfo info) => - (TAsset)ctorInfo.Invoke([registry, guid, info]), locality); - } - - internal static Asset Construct(IAssetRegistry registry, Guid assetId, in TInfo info) - { - EnsureRegistered(); - return constructor!(registry, assetId, info); - } - - private static void EnsureRegistered() - { - if (constructor == null) - throw new InvalidOperationException($"Asset type with info {typeof(TInfo).FullName} was used before being registered"); - } - - internal static Guid ToGuid(in TInfo info) - { - EnsureRegistered(); - if (Locality is AssetLocality.SingleUsage) - // as single usage we should not save the GUID as to not leak memory - return Guid.NewGuid(); - lock (@lock) - { - if (infoToGuid.TryGetValue(info, out var guid)) - return guid; - do - { - guid = Guid.NewGuid(); - } while (guidToInfo.ContainsKey(guid)); - infoToGuid.Add(info, guid); - guidToInfo.Add(guid, info); - return guid; - } - } - - internal static TInfo ToInfo(Guid assetId) - { - EnsureRegistered(); - lock(@lock) - { - if (guidToInfo.TryGetValue(assetId, out var info)) - return info; - throw new KeyNotFoundException($"Could not find registered info for {assetId}"); - } - } -} diff --git a/zzre.core/assetregistry/AssetLocalRegistry.cs b/zzre.core/assetregistry/AssetLocalRegistry.cs deleted file mode 100644 index 4af98f9f..00000000 --- a/zzre.core/assetregistry/AssetLocalRegistry.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace zzre; - -/// A local registry to enable loading of local assets -public sealed class AssetLocalRegistry : zzio.BaseDisposable, IAssetRegistryDebug -{ - private readonly IAssetRegistry globalRegistry; - private readonly AssetRegistry localRegistry; - private readonly AssetHandleScope localScope = new(null!); // the scope will not be used to load, null is a canary value for this - - IAssetRegistryInternal IAssetRegistry.InternalRegistry => localRegistry; - /// - public ITagContainer DIContainer => localRegistry.DIContainer; - - /// - public bool DelayDisposals - { - get => localScope.DelayDisposals; - set => localScope.DelayDisposals = value; - } - - /// - public AssetRegistryStats Stats => globalRegistry.Stats + localRegistry.Stats; - /// - public AssetRegistryStats LocalStats => localRegistry.Stats; - - /// Constructs a new local registry - /// A name for debugging purposes (used in logs) - /// The used for loading asset contents - public AssetLocalRegistry(string debugName, ITagContainer diContainer) - { - globalRegistry = diContainer.GetTag(); - if (globalRegistry is not IAssetRegistryInternal { IsLocalRegistry: false }) - throw new ArgumentException("Registry given to local registry is not a global registry"); - localRegistry = new AssetRegistry(debugName, diContainer, this); - } - - private IAssetRegistry RegistryFor() where TInfo : IEquatable => - AssetInfoRegistry.Locality == AssetLocality.Global ? globalRegistry : localRegistry; - - /// - public unsafe AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - delegate* applyFnptr, - in TApplyContext applyContext) - where TInfo : IEquatable - { - var registry = RegistryFor(); - var handle = registry.Load(in info, priority, applyFnptr, in applyContext); - return new(registry, localScope, handle.AssetID); - } - - /// - public AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - Action? applyAction = null) - where TInfo : IEquatable - { - var registry = RegistryFor(); - var handle = registry.Load(in info, priority, applyAction); - return new(registry, localScope, handle.AssetID); - } - - /// - public void ApplyAssets() => localRegistry.ApplyAssets(); - - void IAssetRegistryDebug.CopyDebugInfo(List assetInfos) => - (localRegistry as IAssetRegistryDebug).CopyDebugInfo(assetInfos); - - protected override void DisposeManaged() - { - localScope?.Dispose(); - localRegistry?.Dispose(); - } -} diff --git a/zzre.core/assetregistry/AssetRegistry,Debug.cs b/zzre.core/assetregistry/AssetRegistry,Debug.cs deleted file mode 100644 index ff57d12e..00000000 --- a/zzre.core/assetregistry/AssetRegistry,Debug.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace zzre; - -/// Enables additional, not-particularly-efficient access to the state of registries for debugging purposes -public interface IAssetRegistryDebug : IAssetRegistry -{ - /// A snapshot of an assets state - /// The asset ID - /// The type of the asset instance - /// The debugging name of the asset - /// The reference count of the asset - /// The loading state of the asset - /// The *effective* load priority of the asset - public readonly record struct AssetInfo( - Guid ID, - Type Type, - string Name, - int RefCount, - AssetState State, - AssetLoadPriority Priority); - - /// Whether this registry was already disposed - bool WasDisposed { get; } - /// Creats a snapshot of the state of all, currently registered assets - /// The asset states will be copied into this list - void CopyDebugInfo(List assetInfos); -} - -partial class AssetRegistry : IAssetRegistryDebug -{ - void IAssetRegistryDebug.CopyDebugInfo(List assetInfos) - { - lock (assets) - { - assetInfos.Clear(); - assetInfos.EnsureCapacity(assetInfos.Count + assets.Values.Count); - foreach (var asset in assets.Values) - { - assetInfos.Add(new( - asset.ID, - asset.GetType(), - asset.ToString() ?? "", - asset.RefCount, - asset.State, - asset.Priority)); - } - } - } -} diff --git a/zzre.core/assetregistry/AssetRegistry.ECS.cs b/zzre.core/assetregistry/AssetRegistry.ECS.cs deleted file mode 100644 index 85d45de3..00000000 --- a/zzre.core/assetregistry/AssetRegistry.ECS.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace zzre; - -partial class AssetRegistry -{ - /// - /// Adds watchers to automatically dispose components when they are removed from an entity or the world is disposed - /// - /// The world to register at - public static void SubscribeAt(DefaultEcs.World world) - { - world.SubscribeEntityComponentRemoved(HandleAssetHandleRemoved); - world.SubscribeEntityComponentRemoved(HandleAssetHandlesRemoved); - } - - private static void HandleAssetHandleRemoved(in DefaultEcs.Entity entity, in AssetHandle handle) => - handle.Dispose(); - - private static void HandleAssetHandlesRemoved(in DefaultEcs.Entity entity, in AssetHandle[] handles) - { - foreach (var handle in handles) - handle.Dispose(); - } -} diff --git a/zzre.core/assetregistry/AssetRegistry.Internal.cs b/zzre.core/assetregistry/AssetRegistry.Internal.cs deleted file mode 100644 index 77312442..00000000 --- a/zzre.core/assetregistry/AssetRegistry.Internal.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace zzre; - -partial class AssetRegistry -{ - CancellationToken IAssetRegistryInternal.Cancellation => cancellationSource.Token; - bool IAssetRegistryInternal.IsLocalRegistry => apparentRegistry != this; - - void IAssetRegistryInternal.DisposeHandle(AssetHandle handle) - { - if (handle.Registry != this) - throw new ArgumentException("Tried to unload asset at wrong registry"); - lock (assets) - { - if (assets.TryGetValue(handle.AssetID, out var asset)) - asset.DelRef(); - } - } - - private IAsset? TryGetForApplying(AssetHandle handle) - { - lock (assets) - { - var asset = assets.GetValueOrDefault(handle.AssetID); - if (asset is not null) - { - asset.ThrowIfError(); - if (asset.State == AssetState.Disposed) - asset = null; - } - return asset; - } - } - - unsafe void IAssetRegistryInternal.AddApplyAction(AssetHandle handle, - delegate* managed applyFnptr, - in TApplyContext applyContext) - { - var asset = TryGetForApplying(handle); - if (asset is null) - return; - lock (asset) - { - if (asset.State == AssetState.Loaded && IsMainThread) - applyFnptr(handle, in applyContext); - else - { - asset.ApplyAction.Next += ConvertFnptr(applyFnptr, in applyContext); - if (asset.State == AssetState.Loaded) - assetsToApply.Writer.WriteAsync(asset, Cancellation).AsTask().Wait(); - } - } - } - - void IAssetRegistryInternal.AddApplyAction(AssetHandle handle, - IAssetRegistry.ApplyWithContextAction applyAction, - in TApplyContext applyContext) - { - var asset = TryGetForApplying(handle); - if (asset is null) - return; - lock (asset) - { - if (asset.State == AssetState.Loaded && IsMainThread) - applyAction(handle, in applyContext); - else - { - var applyContextCopy = applyContext; - asset.ApplyAction.Next += handle => applyAction(handle, in applyContextCopy); - if (asset.State == AssetState.Loaded) - assetsToApply.Writer.WriteAsync(asset, Cancellation).AsTask().Wait(); - } - } - } - - void IAssetRegistryInternal.AddApplyAction(AssetHandle handle, - Action applyAction) - { - var asset = TryGetForApplying(handle); - if (asset is null) - return; - lock (asset) - { - if (asset.State == AssetState.Loaded && IsMainThread) - applyAction(handle); - else - { - asset.ApplyAction.Next += applyAction; - if (asset.State == AssetState.Loaded) - assetsToApply.Writer.WriteAsync(asset, Cancellation).AsTask().Wait(); - } - } - } - - ValueTask IAssetRegistryInternal.QueueRemoveAsset(IAsset asset) - { - stats.OnAssetRemoved(); - if (IsMainThread) - { - RemoveAsset(asset); - return ValueTask.CompletedTask; - } - else - return assetsToRemove.Writer.WriteAsync(asset, Cancellation); - } - - ValueTask IAssetRegistryInternal.QueueApplyAsset(IAsset asset) - { - stats.OnAssetLoaded(); - if (IsMainThread) - { - ApplyAsset(asset); - return ValueTask.CompletedTask; - } - else - return assetsToApply.Writer.WriteAsync(asset, Cancellation); - } - - Task IAssetRegistry.WaitAsyncAll(AssetHandle[] secondaryHandles) - { - lock (assets) - { - foreach (var handle in secondaryHandles) - { - if (assets.TryGetValue(handle.AssetID, out var asset)) - asset.StartLoading(); - } - return Task.WhenAll(secondaryHandles.Select(h => assets[h.AssetID].LoadTask)); - } - } - - bool IAssetRegistryInternal.IsLoaded(Guid assetId) - { - EnsureMainThread(); - IAsset? asset; - lock (assets) - asset = assets.GetValueOrDefault(assetId); - if (asset == null) - return false; - lock (asset) - return asset.State == AssetState.Loaded; - } - - TAsset IAssetRegistryInternal.GetLoadedAsset(Guid assetId) - { - IAsset? asset; - lock (assets) - asset = assets.GetValueOrDefault(assetId); - if (asset == null) - throw new InvalidOperationException("Asset is not present in registry"); - if (asset is not TAsset) - throw new InvalidOperationException($"Asset is not of type {typeof(TAsset).Name}"); - lock (asset) - { - asset.ThrowIfError(); - switch (asset.State) - { - case AssetState.Disposed: - throw new ObjectDisposedException(asset.ToString()); - case AssetState.Loaded: - return (TAsset)asset; - default: - if (!IsMainThread) - throw new InvalidOperationException("Cannot synchronously wait for assets to load on secondary threads"); - asset.Complete(); - asset.ApplyAction.Invoke(new(this, assetId)); - return (TAsset)asset; - } - } - } -} diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 3748991b..80567fc1 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -1,19 +1,42 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using DotNext.Threading; using Serilog; +using Serilog.Core; namespace zzre; -/// A global asset registry to facilitate loading, retrieval and disposal of assets -public sealed partial class AssetRegistry : zzio.BaseDisposable, IAssetRegistryInternal +internal sealed class AssetState { - private static readonly int MaxLowPriorityAssetsPerFrame = Math.Max(1, Environment.ProcessorCount); + public required bool NeedsMainThreadDisposal; + public required AsyncLazy LoadLazy; + public required uint Tag; // used for apply actions to prevent triggering from revived assets + public required Type AssetType; + public IDisposable? Asset + { + get + { + var result = LoadLazy.Value; + return result is null || !result.Value.IsSuccessful + ? null : result.Value.Value; + } + } + public int RefCount = 1; + public AssetPriority Priority; + public string? Name; +} + +public class AssetRegistry : IAssetRegistryInternal +{ + private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(3); + internal static readonly AsyncLazy NullAssetLoadLazy = new(NullDisposable.Instance); + private static readonly int MaxLowPriorityAssetsPerFrame = Math.Clamp(Environment.ProcessorCount, 1, 64); private static readonly UnboundedChannelOptions ChannelOptions = new() { AllowSynchronousContinuations = true, @@ -21,221 +44,495 @@ public sealed partial class AssetRegistry : zzio.BaseDisposable, IAssetRegistryI SingleWriter = false }; - private readonly ILogger logger; - private readonly int mainThreadId = Environment.CurrentManagedThreadId; - /// The apparent registry is the interface given to assets, it will differ for local registriess - private readonly IAssetRegistry apparentRegistry; - private readonly Dictionary assets = []; + private readonly Channel assetsToStart = Channel.CreateUnbounded(ChannelOptions); + private readonly Channel assetsToDispose = Channel.CreateUnbounded(ChannelOptions); + private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); - private readonly Channel assetsToRemove = Channel.CreateUnbounded(ChannelOptions); - private readonly Channel assetsToApply = Channel.CreateUnbounded(ChannelOptions); - private readonly Channel assetsToStart = Channel.CreateUnbounded(ChannelOptions); - private AssetRegistryStats stats; - - private CancellationToken Cancellation => cancellationSource.Token; - private bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; - /// - public ITagContainer DIContainer { get; } - /// - public AssetRegistryStats Stats => stats; + private readonly IAssetRegistryLock mainLock = new TrackingAssetLock(new DotNextAsyncAssetLock()); + private readonly ILogger logger; + private readonly int mainThreadId; + private readonly Dictionary> applyActionCaster = []; + private List<(Guid assetId, uint tag, Type assetType, object action)> applyActions = [], applyActionsBackup = []; + private uint nextAssetTag; + private AssetRegistryStats localStats; - /// Constructs a global asset registry - /// A name for debugging purposes (used in logs) - /// The given to this registry for loading the asset contents - public AssetRegistry(string debugName, ITagContainer diContainer) : this(debugName, diContainer, null) { } + public bool WasDisposed => cancellationSource.IsCancellationRequested; + public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; + public bool IsLocalRegistry => ParentRegistry is not null; + public IAssetRegistry? ParentRegistry { get; } + public CancellationToken Cancellation => cancellationSource.Token; + public ITagContainer DIContainer { get; } + public AssetRegistryStats Stats => (ParentRegistry?.Stats ?? default) + localStats; - internal AssetRegistry(string debugName, ITagContainer diContainer, IAssetRegistry? apparentRegistry) + public AssetRegistry(ITagContainer diContainer, IAssetRegistry? parent = null, string? debugName = null) { DIContainer = diContainer; - this.apparentRegistry = apparentRegistry ?? this; - if (string.IsNullOrEmpty(debugName)) - logger = diContainer.GetLoggerFor(); - else - logger = diContainer.GetTag().For($"{nameof(AssetRegistry)}-{debugName}"); + ObjectDisposedException.ThrowIf(parent is { WasDisposed: true }, typeof(AssetRegistry)); + if (parent is { IsLocalRegistry: true }) + throw new ArgumentException("Cannot use a local registry as parent"); + ParentRegistry = parent; + logger = // ILogger is optional, as well as the log prefix + !diContainer.TryGetTag(out var parentLogger) ? Logger.None + : string.IsNullOrEmpty(debugName) ? diContainer.GetLoggerFor() + : diContainer.GetTag().For($"{nameof(AssetRegistry)}-{debugName}"); + mainThreadId = Environment.CurrentManagedThreadId; } - protected override void DisposeManaged() + public void Dispose() { - EnsureMainThread(); + if (WasDisposed) + return; + if (mainLock.Wait(LockTimeout, Cancellation)) + logger.Warning("AssetRegistry could not lock during dispose, going ahead nonetheless"); cancellationSource.Cancel(); - Task.WhenAll(assets.Values - .Where(a => a.State is AssetState.Loading or AssetState.LoadingSecondary) - .Select(a => a.LoadTask)) - .Wait(10000); foreach (var asset in assets.Values) - asset.Dispose(); + { + DisposeAssetState(asset); + } assets.Clear(); - assetsToRemove.Writer.Complete(); - assetsToApply.Writer.Complete(); - assetsToStart.Writer.Complete(); + applyActions.Clear(); + applyActionsBackup.Clear(); + assetsToDispose.Writer.TryComplete(); + assetsToStart.Writer.TryComplete(); + DisposeOldAssets(); // after current assets in case we add something into it (we shouldn't) + + mainLock.Dispose(); cancellationSource.Dispose(); logger.Verbose("Finished disposing registry"); } - private IAsset GetOrCreateAsset(in TInfo info) - where TInfo : IEquatable + private void DisposeAssetObject(bool needsMainThreadDisposal, IDisposable? assetObject) { - Cancellation.ThrowIfCancellationRequested(); - if (AssetInfoRegistry.Locality is not AssetLocality.Global && apparentRegistry == this) - throw new InvalidOperationException("Cannot retrieve or create local assets in a global asset registry"); + if (assetObject is null) + return; + if (IsMainThread || !needsMainThreadDisposal) + { + localStats.OnAssetRemoved(); + assetObject.Dispose(); + } + else + { + var success = assetsToDispose.Writer.TryWrite(assetObject); + Debug.Assert(success); + } + } + + private void DisposeAssetState(AssetState state) + { + DisposeAssetObject(state.NeedsMainThreadDisposal, state.Asset); + state.RefCount = 0; + state.LoadLazy = NullAssetLoadLazy; + } + + [ExcludeFromCodeCoverage] // we cannot reasonably check for semaphore failure + private IAssetRegistryLock.Releaser LockSemaphore([CallerMemberName] string context = "") + { + var releaser = mainLock.Wait(LockTimeout, Cancellation, context); + if (!releaser) + throw new InvalidOperationException("Could not lock asset registry"); + return releaser; + // this should only happen in bug scenarios + } + + [ExcludeFromCodeCoverage] + private async Task LockSemaphoreAsync(CancellationToken ct, [CallerMemberName] string context = "") + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(Cancellation, ct); + var releaser = await mainLock.WaitAsync(LockTimeout, cts.Token, context); + if (!releaser) + throw new InvalidOperationException("Could not lock asset registry"); + return releaser; + } - var guid = AssetInfoRegistry.ToGuid(info); - lock(assets) + void IAssetRegistryInternal.AddRef(Guid assetId) + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + using var _ = LockSemaphore(); + ObjectDisposedException.ThrowIf(!TryAddRefUnsafe(assetId), typeof(IAsset)); + } + + private bool TryAddRefUnsafe(Guid assetId) + { + Debug.Assert(!WasDisposed); + //Debug.Assert(semaphore.CurrentCount == 0); + var assetState = assets.GetValueOrDefault(assetId); + if (assetState is null || assetState.RefCount <= 0) + return false; + assetState.RefCount++; + return true; + } + + [ExcludeFromCodeCoverage] + void IAssetRegistryInternal.DelRef(Guid assetId) + { + if (WasDisposed) return; // Ignore out-of-order deletion, all assets are already dead + using var releaser = LockSemaphore(); + DelRefUnsafe(assetId); + } + + private void DelRefUnsafe(Guid assetId) + { + Debug.Assert(!WasDisposed); + //Debug.Assert(semaphore.CurrentCount == 0); + var assetState = assets.GetValueOrDefault(assetId); + if (assetState is null || assetState.RefCount <= 0) + return; // Let's just ignore already-dead assets, we got what we wanted + if (--assetState.RefCount <= 0) + { + DisposeAssetState(assetState); + assets.Remove(assetId); + } + } + + AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) + { + AssetState asset; + using var releaser = LockSemaphore(); + ObjectDisposedException.ThrowIf(!assets.TryGetValue(assetId, out asset!), nameof(IAssetHandle)); + return asset.LoadLazy; + } + + void IAssetRegistryInternal.CheckType(Guid assetId, Type type) + { + using var releaser = LockSemaphore(); + ObjectDisposedException.ThrowIf(!assets.TryGetValue(assetId, out var asset) || asset.RefCount <= 0, nameof(IAssetHandle)); + if (!asset.AssetType.IsAssignableTo(type)) + throw new InvalidCastException($"Cannot cast asset of type {asset.AssetType.FullName} to {type.FullName}"); + } + + public AssetHandle Load(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IAsset + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (TAsset.Locality is not AssetLocality.Global && !IsLocalRegistry) + throw new ArgumentException($"Cannot load a local asset {typeof(TAsset).FullName} from a global registry"); + if (TAsset.Locality is AssetLocality.Global && IsLocalRegistry) + return ParentRegistry!.Load(info, priority); + + var (assetId, assetState) = GetOrCreateAssetState(info, priority); + var handle = new AssetHandle(this, assetId); + if (!assetState.LoadLazy.IsValueCreated) { - if (!assets.TryGetValue(guid, out var asset) || asset.State is AssetState.Disposed) + switch (priority) { - logger.Verbose("New {Type} asset {Info} ({ID})", AssetInfoRegistry.Name, info, guid); - stats.OnAssetCreated(); - asset = AssetInfoRegistry.Construct(apparentRegistry, guid, in info); - assets[guid] = asset; - return asset; + case AssetPriority.Synchronous: + try + { + handle.Get(); // checks main thread + } + catch + { + // the user does not get the handle, so there shouldn't be a refcount on the asset + (this as IAssetRegistryInternal).DelRef(assetId); + throw; + } + break; + case AssetPriority.High: + Task.Run(() => TryStartLoad(handle.AssetId), Cancellation); + break; + case AssetPriority.Low: + var success = assetsToStart.Writer.TryWrite(assetId); + Debug.Assert(success); // As the channel is unbounded it should never fail to write + break; } - asset.ThrowIfError(); - return asset; } + return handle; } - /// - public unsafe AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - delegate* managed applyFnptr, - in TApplyContext applyContext) - where TInfo : IEquatable + private (Guid, AssetState) GetOrCreateAssetState(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IAsset { - var asset = GetOrCreateAsset(in info); - lock (asset) + using var releaser = LockSemaphore(); + + // Determine Asset ID + Guid assetId; + if (TAsset.Locality is AssetLocality.Unique) { - if (asset is { State: AssetState.Loaded } && IsMainThread) + do { - // fast path: asset is already loaded and we only need to apply it - asset.AddRef(); - var handle = new AssetHandle(this, asset.ID); - applyFnptr(handle, in applyContext); - return handle; - } - return LoadInner(asset, priority, ConvertFnptr(applyFnptr, applyContext)); + assetId = Guid.NewGuid(); + } while (assets.ContainsKey(assetId)); // just paranoid... } + else + assetId = TAsset.InfoToAssetId(info); + + // Check previous asset state + if (assets.TryGetValue(assetId, out var assetState) && assetState.RefCount > 0) + { + SanityCheckSharedAsset(typeof(TAsset), assetState); + assetState.RefCount++; + if (assetState.Asset is null && (int)priority < (int)assetState.Priority) + assetState.Priority = priority; + return (assetId, assetState); + } + + // Create new asset state + TInfo infoCopy = info; + assetState = new() + { + NeedsMainThreadDisposal = TAsset.NeedsMainThreadDisposal, + LoadLazy = new(ct => LoadAsset(infoCopy, assetId)), + Tag = unchecked(++nextAssetTag), + AssetType = typeof(TAsset), + Priority = priority + }; + assets[assetId] = assetState; + localStats.OnAssetCreated(); + return (assetId, assetState); } - private static unsafe Action ConvertFnptr( - delegate* managed fnptr, - in TContext context) + [Conditional("DEBUG")] + private static void SanityCheckSharedAsset(Type expectedType, AssetState asset) { - var contextCopy = context; - return handle => fnptr(handle, in contextCopy); + if (asset.Asset is null) return; + var actualType = asset.Asset.GetType(); + Debug.Assert(actualType.IsAssignableTo(expectedType), "Asset type mismatch, is this a GUID conflict?"); } - /// - public AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - Action? applyAction) - where TInfo : IEquatable + private async Task TryStartLoad(Guid assetId) { - var asset = GetOrCreateAsset(in info); - lock (asset) + AssetState? assetState = null; + using (await LockSemaphoreAsync(Cancellation)) { - if (asset is { State: AssetState.Loaded } && IsMainThread) + assetState = assets.GetValueOrDefault(assetId); + if (assetState is null or { RefCount: <= 0 }) { - asset.AddRef(); - var handle = new AssetHandle(this, asset.ID); - applyAction?.Invoke(handle); - return handle; + // if the High load cannot start because all handles are disposed + // then no one will know we never tried to load it in the first place + return; } - return LoadInner(asset, priority, applyAction); } + + await assetState.LoadLazy.WithCancellation(Cancellation); } - private AssetHandle LoadInner(IAsset asset, AssetLoadPriority priority, Action? applyAction) + private async Task LoadAsset(TInfo info, Guid assetId) + where TInfo : struct, IEquatable + where TAsset : class, IAsset { - // We assume that asset is locked for our thread during this method - asset.AddRef(); - var handle = new AssetHandle(this, asset.ID); - switch(asset.State) + // Due to AsyncLazy we can flow exceptions outside this method + + // Load asset + var asset = (await TAsset.LoadAsync(this, assetId, info, Cancellation)).Asset; + Debug.Assert(asset.Registry == this); + CheckRegistryDisposal(); + + // Propagate assets into registry state + AssetState? assetState = null; + using (await LockSemaphoreAsync(Cancellation)) { - case AssetState.Disposed or AssetState.Error: - throw new ArgumentException("LoadInner was called with asset in unexpected state"); + assetState = assets.GetValueOrDefault(assetId); + if (assetState is null or { RefCount: <= 0 }) + assetState = null; + else + localStats.OnAssetLoaded(); + } - case AssetState.Queued or AssetState.Loading or AssetState.LoadingSecondary: - if (applyAction is not null) - asset.ApplyAction.Next += applyAction; - if (asset.State == AssetState.Queued) - StartLoading(asset, priority); - return handle; + if (assetState is null) + { + // handle was disposed during load, now dispose the asset itself + DisposeAssetObject(TAsset.NeedsMainThreadDisposal, asset); + ObjectDisposedException.ThrowIf(true, typeof(TAsset)); + } - case AssetState.Loaded: - if (IsMainThread) - applyAction?.Invoke(handle); - else if (applyAction is not null) - assetsToApply.Writer.WriteAsync(asset, Cancellation).AsTask().WaitAndRethrow(); - return handle; + CheckRegistryDisposal(); + return asset; - default: throw new NotImplementedException($"Unimplemented asset state {asset.State}"); + [ExcludeFromCodeCoverage] // we cannot reasonably test that, it would be a race condition + void CheckRegistryDisposal() + { + if (WasDisposed) + { + if (!TAsset.NeedsMainThreadDisposal) + asset.Dispose(); + // otherwise we are in a predicament and my decision is to leak a couple assets + ObjectDisposedException.ThrowIf(true, typeof(AssetRegistry)); + } + Cancellation.ThrowIfCancellationRequested(); } } - private void StartLoading(IAsset asset, AssetLoadPriority priority) + public bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset { - // We assume that asset is locked for our thread during this method - asset.Priority = priority; - switch (priority) - { - case AssetLoadPriority.Synchronous: - if (!asset.ApplyAction.IsEmpty && !IsMainThread) - throw new InvalidOperationException("Cannot load assets with Apply functions synchronously on secondary threads"); - asset.Complete(); - asset.ApplyAction.Invoke(new(this, asset.ID)); - break; - case AssetLoadPriority.High: - asset.StartLoading(); - break; - case AssetLoadPriority.Low: - assetsToStart.Writer.WriteAsync(asset, Cancellation).AsTask().WaitAndRethrow(); - break; - default: throw new NotImplementedException($"Unimplemented asset load priority {priority}"); - } + ObjectDisposedException.ThrowIf(WasDisposed, typeof(AssetRegistry)); + if (ParentRegistry?.TryGet(assetId, out handle) is true) + return true; + + handle = default; + using var releaser = LockSemaphore(); + if (!assets.TryGetValue(assetId, out var assetState) || + assetState.RefCount < 1 || + !assetState.AssetType.IsAssignableTo(typeof(TAsset))) + return false; + + assetState.RefCount++; + handle = new(this, assetId); + return true; } - private void RemoveAsset(IAsset asset) + public void Update() => + Update(MaxLowPriorityAssetsPerFrame); + + public void Update(int maxLowPrioAssets) { - if (asset.State is not (AssetState.Disposed or AssetState.Error)) - throw new InvalidOperationException($"Unexpected asset state for removal: {asset.State}"); - lock (assets) - assets.Remove(asset.ID); - logger.Verbose("Remove asset {Type} {ID}", asset.GetType().Name, asset.ID); + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (!IsMainThread) + throw new InvalidOperationException("Low batch scheduling is only allowed on the main thread"); + + using (LockSemaphore()) + { + DisposeOldAssets(); + + for (int i = 0; i < maxLowPrioAssets && assetsToStart.Reader.TryRead(out var assetId); i++) + { + if (assets.TryGetValue(assetId, out var assetState) && + assetState.LoadLazy != NullAssetLoadLazy && + !assetState.LoadLazy.IsValueCreated) + Task.Run(() => assetState.LoadLazy.WithCancellation(Cancellation), Cancellation); + } + + // Copy apply actions and make sure assets keep alive during applying + (applyActions, applyActionsBackup) = (applyActionsBackup, applyActions); + for (int i = 0; i < applyActionsBackup.Count; i++) + { + var assetId = applyActionsBackup[i].assetId; + if (!TryAddRefUnsafe(assetId)) + // Asset is not alive anymore + applyActionsBackup[i] = default; + else if (assets[assetId].Tag != applyActionsBackup[i].tag) + { + // Asset was revived, apply actions are outdated + DelRefUnsafe(assetId); + applyActionsBackup[i] = default; + } + else if (assets[assetId].Asset is null) + { + // Asset was not yet loaded + DelRefUnsafe(assetId); + applyActions.Add(applyActionsBackup[i]); + applyActionsBackup[i] = default; + } + } + } + + // We can safely access applyActionsBackup as we are on the main thread + var exceptions = new List(); + foreach (var (assetId, _, assetType, action) in applyActionsBackup) + { + if (assetId == default) + continue; + try + { + applyActionCaster[assetType](assetId, action); + } + catch (Exception e) + { + exceptions.Add(e); + } + } + + // Remove reference we added earlier + using (LockSemaphore()) + { + foreach (var (assetId, _, _, _) in applyActionsBackup) + { + if (assetId != default) + DelRefUnsafe(assetId); + } + } + applyActionsBackup.Clear(); + + if (exceptions.Count > 0) + throw new AggregateException(exceptions); } - private void ApplyAsset(IAsset asset) + private void DisposeOldAssets() { - if (!asset.LoadTask.IsCompleted) - throw new InvalidOperationException("Cannot apply assets that are not (internally) loaded"); - asset.ApplyAction.Invoke(new(this, asset.ID)); + Debug.Assert(IsMainThread); + //Debug.Assert(semaphore.CurrentCount == 0); + while (assetsToDispose.Reader.TryRead(out var asset)) + { + asset.Dispose(); + localStats.OnAssetRemoved(); + } } - /// - public void ApplyAssets() + public void Apply(AssetHandle handle, Action> action) + where TAsset : class, IAsset { - EnsureMainThread(); - while (assetsToRemove.Reader.TryRead(out var asset)) - RemoveAsset(asset); - while (assetsToApply.Reader.TryRead(out var asset)) - ApplyAsset(asset); + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (handle.Registry is null) + throw new ArgumentException("Invalid asset handle"); + if (handle.Registry == ParentRegistry) + { + ParentRegistry.Apply(handle, action); + return; + } + if (handle.Registry != this) + throw new ArgumentException("Asset is not part of this registry or its parent"); + + bool shouldBeExecutedNow = false; + + using (LockSemaphore()) + { + if (!applyActionCaster.ContainsKey(typeof(TAsset))) + { + applyActionCaster.Add(typeof(TAsset), (assetId, action) => + { + ((Action>)action)(new(this, assetId)); + }); + } + + if (IsMainThread) + { + ObjectDisposedException.ThrowIf(!TryAddRefUnsafe(handle.AssetId), typeof(IAsset)); + if (assets[handle.AssetId].Asset is null) + DelRefUnsafe(handle.AssetId); // Asset was not yet loaded + else + shouldBeExecutedNow = true; + } + if (!shouldBeExecutedNow) + applyActions.Add(new(handle.AssetId, assets[handle.AssetId].Tag, typeof(TAsset), action)); + } - for (int i = 0; i < MaxLowPriorityAssetsPerFrame && assetsToStart.Reader.TryRead(out var asset); i++) + // Fast-path: no queueing + if (shouldBeExecutedNow) { - lock (asset) + try { - if (asset.State == AssetState.Queued) - asset.StartLoading(); + action(new(this, handle.AssetId)); + } + finally + { + (this as IAssetRegistryInternal).DelRef(handle.AssetId); } } } - [Conditional("DEBUG")] - private void EnsureMainThread([CallerMemberName] string methodName = "") + public void CopyDebugInfo(List infos) { - if (!IsMainThread) - throw new InvalidOperationException($"Cannot call AssetRegistry.{methodName} from secondary threads"); + using (LockSemaphore()) + { + infos.Clear(); + infos.EnsureCapacity(assets.Count); + foreach (var (assetId, state) in assets) + { + var name = + state.Name ?? + (state.Name = state.Asset?.ToString()) ?? + $"Loading {state.AssetType.Name}"; + infos.Add(new( + assetId, + state.AssetType, + name, + state.RefCount, + state.Asset is not null, + state.Priority + )); + } + } } } diff --git a/zzre.core/assetregistry/AssetRegistryDelayed.cs b/zzre.core/assetregistry/AssetRegistryDelayed.cs new file mode 100644 index 00000000..c84e65ea --- /dev/null +++ b/zzre.core/assetregistry/AssetRegistryDelayed.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using DotNext.Threading; + +namespace zzre; + +public sealed class AssetRegistryDelayed(IAssetRegistry Inner) : IAssetRegistryInternal +{ + [ExcludeFromCodeCoverage] + public bool WasDisposed => Inner.WasDisposed; + [ExcludeFromCodeCoverage] + public bool IsMainThread => Inner.IsMainThread; + [ExcludeFromCodeCoverage] + public ITagContainer DIContainer => Inner.DIContainer; + [ExcludeFromCodeCoverage] + public IAssetRegistry? ParentRegistry => Inner.ParentRegistry; + [ExcludeFromCodeCoverage] + public bool IsLocalRegistry => Inner.IsLocalRegistry; + [ExcludeFromCodeCoverage] + public CancellationToken Cancellation => Inner.Cancellation; + [ExcludeFromCodeCoverage] + public AssetRegistryStats Stats => Inner.Stats; + + [ExcludeFromCodeCoverage] + public void CopyDebugInfo(List assetInfos) => + Inner.CopyDebugInfo(assetInfos); + + [ExcludeFromCodeCoverage] + public void Dispose() => Inner.Dispose(); + [ExcludeFromCodeCoverage] + public void Update() => Inner.Update(); + + [ExcludeFromCodeCoverage] + void IAssetRegistryInternal.AddRef(Guid assetId) => + ((IAssetRegistryInternal)Inner).AddRef(assetId); + + [ExcludeFromCodeCoverage] + public void Apply(AssetHandle handle, Action> action) + where TAsset : class, IAsset => + Inner.Apply(handle, action); + + [ExcludeFromCodeCoverage] + void IAssetRegistryInternal.CheckType(Guid assetId, Type type) => + ((IAssetRegistryInternal)Inner).CheckType(assetId, type); + + [ExcludeFromCodeCoverage] + AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) => + ((IAssetRegistryInternal)Inner).GetAsset(assetId); + + [ExcludeFromCodeCoverage] + public bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset => + Inner.TryGet(assetId, out handle); + + public bool DelayDisposals + { + get => Volatile.Read(ref delayDeletion); + set + { + Volatile.Write(ref delayDeletion, value); + if (!value || Inner.WasDisposed) + return; + lock (assetIdsToDelete) + { + foreach (var id in assetIdsToDelete) + ((IAssetRegistryInternal)Inner).DelRef(id); + assetIdsToDelete.Clear(); + } + } + } + + private readonly List assetIdsToDelete = new(64); + private bool delayDeletion; + + public AssetHandle Load(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IAsset + { + var parentHandle = Inner.Load(info, priority); + Debug.Assert(parentHandle.Asset == parentHandle.Asset); // checks that the handle is not disposed + return new(this, parentHandle.AssetId, false); + } + + void IAssetRegistryInternal.DelRef(Guid assetId) + { + if (DelayDisposals) + { + lock (assetIdsToDelete) + assetIdsToDelete.Add(assetId); + } + else + ((IAssetRegistryInternal)Inner).DelRef(assetId); + } +} diff --git a/zzre.core/assetregistry/AssetRegistryStats.cs b/zzre.core/assetregistry/AssetRegistryStats.cs index 5cfdcf04..e27fab87 100644 --- a/zzre.core/assetregistry/AssetRegistryStats.cs +++ b/zzre.core/assetregistry/AssetRegistryStats.cs @@ -6,22 +6,22 @@ namespace zzre; /// /// A snapshot of mostly monotonous counters of an /// -public struct AssetRegistryStats +public struct AssetRegistryStats(int created, int loaded, int removed, int total) { - private int created; - private int loaded; - private int removed; - private int total; + private int created = created; + private int loaded = loaded; + private int removed = removed; + private int total = total; /// The number of assets created - public int Created => created; + public readonly int Created => created; /// The number of assets that finished loading - public int Loaded => loaded; + public readonly int Loaded => loaded; /// The number of assets removed from the registry - public int Removed => removed; + public readonly int Removed => removed; /// The number of currently registered assets /// This counter is not monotonous - public int Total => total; + public readonly int Total => total; internal void OnAssetCreated() { @@ -53,7 +53,7 @@ internal void OnAssetRemoved() total = rhs.total + lhs.total, }; - public override string ToString() + public override readonly string ToString() { var builder = new StringBuilder(256); builder.Append("Created: "); @@ -62,6 +62,8 @@ public override string ToString() builder.Append(loaded); builder.Append(" Removed: "); builder.Append(removed); + builder.Append(" Total: "); + builder.Append(total); return builder.ToString(); } } diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs new file mode 100644 index 00000000..6138c445 --- /dev/null +++ b/zzre.core/assetregistry/IAsset.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace zzre; + +public enum AssetLocality +{ + Global, + Local, + Unique +} + +public interface IAsset : IDisposable +{ + [ExcludeFromCodeCoverage] // for some reason not called + public static virtual AssetLocality Locality => AssetLocality.Global; + public static virtual bool NeedsMainThreadDisposal => false; + IAssetRegistry Registry { get; } +} + +public readonly record struct AssetLoadResult( + IAsset Asset +) where TInfo : struct, IEquatable; + +public interface IAsset : IAsset + where TInfo : struct, IEquatable +{ + static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); + static abstract Task> LoadAsync(IAssetRegistry registry, Guid assetId, TInfo info, CancellationToken ct); + + private static readonly object generalInfoLock = new(); + private static readonly Dictionary generalInfoToGuid = []; + internal static Guid GeneralInfoToGuid(in TInfo info) + { + lock (generalInfoLock) + { + if (generalInfoToGuid.TryGetValue(info, out var guid)) + return guid; + generalInfoToGuid.Add(info, guid = Guid.NewGuid()); + return guid; + } + } +} + diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 21bb0d4f..83c30918 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -1,96 +1,63 @@ -using System; -using System.Threading.Tasks; +using System; +using System.Collections.Generic; using System.Threading; +using DotNext.Threading; + namespace zzre; -/// Controls when an asset is actually loaded -public enum AssetLoadPriority +public enum AssetPriority { - /// The asset will be completly loaded before the Load method returns Synchronous, - /// Loading will be started immediately but may finish asynchronously High, - /// Loading will be started at a later point and will always finish asynchronously Low } -/// A facilitates loading, retrieval and disposal of assets -/// This interface may point to a local or a global registry public interface IAssetRegistry : IDisposable { - public delegate void ApplyWithContextAction(AssetHandle handle, in TApplyContext context); - - internal IAssetRegistryInternal InternalRegistry { get; } - /// The given to this registry at construction to be used for loading the asset contents + bool WasDisposed { get; } + bool IsMainThread { get; } ITagContainer DIContainer { get; } - /// Basic statistics of the registry, containing mostly monotonous counters - /// This property will include the statistics of parent registries + IAssetRegistry? ParentRegistry { get; } + bool IsLocalRegistry { get; } + CancellationToken Cancellation { get; } // is triggered when registry is disposed AssetRegistryStats Stats { get; } - /// Basic statistics of the registry, containing mostly monotonous counters - /// This property will not include statistics of parent registries - AssetRegistryStats LocalStats => Stats; - - /// Registers an asset for loading or returns a handle to a previously-registered asset - /// Depending on whether the asset is already loaded the apply action will be called immediately or only stored for later execution - /// The type that will determine the asset type - /// The type of the apply context given to the apply action - /// The value identifying the specific asset to load - /// The load priority of the asset (ignored if already loaded) - /// The function pointer to call as apply action - /// The apply context given to the apply action - /// An untyped to the asset that controlles the lifetime of the asset instance - unsafe AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - delegate* managed applyFnptr, - in TApplyContext applyContext) - where TInfo : IEquatable; - - /// Registers an asset for loading or returns a handle to a previously-registered asset - /// Depending on whether the asset is already loaded the apply action will be called immediately or only stored for later execution - /// The type that will determine the asset type - /// The value identifying the specific asset to load - /// The load priority of the asset (ignored if already loaded) - /// The delegate to call as apply action - /// An untyped to the asset that controlles the lifetime of the asset instance - AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - Action? applyAction = null) - where TInfo : IEquatable; - - /// Synchronously applies all outstanding removal or apply action of assets, as well as start loading of low-prioritised assets - /// This method is only to be called from the main thread - void ApplyAssets(); - /// Asynchronously wait for one or more assets to finish loading - /// Use this method to wait for secondary assets - /// Handles to the asset to wait for - /// A task completing when all given assets finish loading - Task WaitAsyncAll(AssetHandle[] assets) => InternalRegistry.WaitAsyncAll(assets); + AssetHandle Load(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IAsset; + + void Apply(AssetHandle handle, Action> action) + where TAsset : class, IAsset; + + bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset; + + void Update(); + + /// A snapshot of an assets state + /// The asset ID + /// The type of the asset instance + /// The debugging name of the asset + /// The reference count of the asset + /// The loading state of the asset + /// The *effective* load priority of the asset + public readonly record struct AssetInfo( + Guid ID, + Type Type, + string Name, + int RefCount, + bool IsLoaded, + AssetPriority Priority); + + /// Creats a snapshot of the state of all, currently registered assets + /// The asset states will be copied into this list + void CopyDebugInfo(List assetInfos); } internal interface IAssetRegistryInternal : IAssetRegistry { - IAssetRegistryInternal IAssetRegistry.InternalRegistry => this; - - bool IsLocalRegistry { get; } - CancellationToken Cancellation { get; } - - unsafe void AddApplyAction(AssetHandle asset, - delegate* managed applyFnptr, - in TApplyContext applyContext); - - void AddApplyAction(AssetHandle asset, - ApplyWithContextAction applyAction, - in TApplyContext applyContext); - - void AddApplyAction(AssetHandle asset, - Action applyAction); - - void DisposeHandle(AssetHandle handle); - ValueTask QueueRemoveAsset(IAsset asset); - ValueTask QueueApplyAsset(IAsset asset); - bool IsLoaded(Guid assetId); - TAsset GetLoadedAsset(Guid assetId); + void AddRef(Guid assetId); + void DelRef(Guid assetId); + AsyncLazy GetAsset(Guid assetId); + void CheckType(Guid assetId, Type type); } diff --git a/zzre.core/assetregistry/IAssetRegistryLock.cs b/zzre.core/assetregistry/IAssetRegistryLock.cs new file mode 100644 index 00000000..27a8fe69 --- /dev/null +++ b/zzre.core/assetregistry/IAssetRegistryLock.cs @@ -0,0 +1,153 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using DotNext.Threading; +using DotNext.Threading.Tasks; + +namespace zzre; + +public interface IAssetRegistryLock : IDisposable +{ + Releaser Wait(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = ""); + Task WaitAsync(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = ""); + + internal void Release(); + public struct Releaser(IAssetRegistryLock? parent) : IDisposable + { + private IAssetRegistryLock? parent = parent; + public void Dispose() + { + parent?.Release(); + parent = null; + } + public static implicit operator bool(in Releaser l) => l.parent is not null; + public static bool operator true(in Releaser l) => l.parent is not null; + public static bool operator false(in Releaser l) => l.parent is null; + + public static Releaser ContinueBoolTask(Task task, object? parent) + => task.Result ? new(parent as IAssetRegistryLock) : default; + + public static Task ConvertFromBoolTask(Task task, IAssetRegistryLock l, CancellationToken ct) => + task.ContinueWith(ContinueBoolTask, l, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); + } +} + +public sealed class SemaphoreAssetLock : IAssetRegistryLock +{ + private readonly SemaphoreSlim semaphore = new(1, 1); + + public void Dispose() => semaphore.Dispose(); + + void IAssetRegistryLock.Release() => semaphore.Release(); + + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, string _) => + semaphore.Wait(timeout, ct) ? new(this) : default; + + public Task WaitAsync(TimeSpan timeout, CancellationToken ct, string _) => + IAssetRegistryLock.Releaser.ConvertFromBoolTask(semaphore.WaitAsync(timeout, ct), this, ct); +} + +// do not use +public sealed class MonitorAssetLock : IAssetRegistryLock +{ + public void Dispose() { } + + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = "") + { + Monitor.Enter(this); + return new(this); + } + + public Task WaitAsync(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = "") + { + Monitor.Enter(this); + return Task.FromResult(new(this)); + } + + void IAssetRegistryLock.Release() + { + Debug.Assert(Monitor.IsEntered(this)); + } +} + +public sealed class DotNextAsyncAssetLock : IAssetRegistryLock +{ + private readonly AsyncExclusiveLock l = new(Environment.ProcessorCount + 1); + + public void Dispose() + { + l.Dispose(); + } + + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = "") + { + l.AcquireAsync(timeout, ct).Wait(); + return new(this); + } + + public async Task WaitAsync(TimeSpan timeout, CancellationToken ct, [CallerMemberName] string context = "") + { + await l.AcquireAsync(timeout, ct); + return new(this); + } + + void IAssetRegistryLock.Release() + { + l.Release(); + } +} + +public sealed class TrackingAssetLock(IAssetRegistryLock inner) : IAssetRegistryLock +{ + private string? last; + + public string? Last => last; + + public void Dispose() + { + inner.Dispose(); + } + + void IAssetRegistryLock.Release() + { + inner.Release(); + Interlocked.Exchange(ref last, null); + last = null; + } + + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, string context) + { + var releaser = inner.Wait(timeout, ct, context); + try + { + if (releaser) + Interlocked.Exchange(ref last, context); + } + catch + { + Debug.Assert(false); + releaser.Dispose(); + throw; + } + return releaser; + } + + public async Task WaitAsync(TimeSpan timeout, CancellationToken ct, string context) + { + var releaser = await inner.WaitAsync(timeout, ct, context); + try + { + if (releaser) + Interlocked.Exchange(ref last, context); + } + catch + { + Debug.Assert(false); + releaser.Dispose(); + throw; + } + return releaser; + } +} diff --git a/zzre.core/math/WorldCollider.cs b/zzre.core/math/WorldCollider.cs index 54036f32..9324ba43 100644 --- a/zzre.core/math/WorldCollider.cs +++ b/zzre.core/math/WorldCollider.cs @@ -86,7 +86,8 @@ public static WorldCollider Create(RWWorld world) Math.Max(1, world.FindAllChildrenById(SectionId.PlaneSection, true).Count()); var rootPlane = world.FindChildById(SectionId.PlaneSection, false) as RWPlaneSection; var rootAtomic = world.FindChildById(SectionId.AtomicSection, false); - var rootSection = rootPlane ?? rootAtomic ?? throw new InvalidDataException("RWWorld has no geometry"); + if (rootPlane is null && rootAtomic is null) + throw new InvalidDataException("RWWorld has no geometry"); // add dummy plane for root atomic worlds rootPlane ??= new RWPlaneSection() @@ -94,7 +95,7 @@ public static WorldCollider Create(RWWorld world) sectorType = RWPlaneSectionType.XPlane, leftValue = float.PositiveInfinity, rightValue = float.PositiveInfinity, - children = [rootAtomic, new RWString()] + children = [rootAtomic!, new RWString()] }; var splits = new CollisionSplit[splitCount]; diff --git a/zzre.core/rendering/StandardTextures.cs b/zzre.core/rendering/StandardTextures.cs index 0ab81d34..bfbe57c5 100644 --- a/zzre.core/rendering/StandardTextures.cs +++ b/zzre.core/rendering/StandardTextures.cs @@ -35,7 +35,7 @@ private unsafe Texture MakeSinglePixel(string name, IColor color) new(1, 1, 1, 1, 1, PixelFormat.R8_G8_B8_A8_UNorm, TextureUsage.Sampled, TextureType.Texture2D)); texture.Name = name; var bytes = stackalloc byte[4]; - *((uint*)bytes) = color.Raw; + *(uint*)bytes = color.Raw; graphicsDevice.UpdateTexture(texture, (nint)bytes, 4, 0, 0, 0, 1, 1, 1, 0, 0); return texture; } diff --git a/zzre.core/zzre.core.csproj b/zzre.core/zzre.core.csproj index a1969c05..e5fc606d 100644 --- a/zzre.core/zzre.core.csproj +++ b/zzre.core/zzre.core.csproj @@ -24,6 +24,7 @@ + diff --git a/zzre/Program.InDev.cs b/zzre/Program.InDev.cs index 1c1a576c..14fb19b0 100644 --- a/zzre/Program.InDev.cs +++ b/zzre/Program.InDev.cs @@ -14,32 +14,32 @@ namespace zzre; internal partial class Program { - private static readonly Option OptionInDevLaunchGame = new Option( + private static readonly Option OptionInDevLaunchGame = new( "--launch-game", () => false, "Launches a game window upon start"); - private static readonly Option OptionInDevLaunchDuel = new Option( + private static readonly Option OptionInDevLaunchDuel = new( "--launch-test-duel", () => false, "Launches a game window with the test duel (ignores savegame) upon start"); - private static readonly Option OptionInDevLaunchGameMuted = new Option( + private static readonly Option OptionInDevLaunchGameMuted = new( "--launch-game-muted", () => false, "Launches the game window muted"); - private static readonly Option OptionInDevLaunchAssetExplorer = new Option( + private static readonly Option OptionInDevLaunchAssetExplorer = new( "--launch-asset-explorer", () => false, "Launches the asset explorer upon start"); - private static readonly Option OptionInDevLaunchECSExplorer = new Option( + private static readonly Option OptionInDevLaunchECSExplorer = new( "--launch-ecs-explorer", () => false, "Launches the ECS explorer upon start"); - private static readonly Option OptionInDevLaunchConfigExplorer = new Option( + private static readonly Option OptionInDevLaunchConfigExplorer = new( "--launch-config-explorer", () => false, "Launches the config explorer upon start"); @@ -97,6 +97,7 @@ private static void HandleInDev(InvocationContext ctx) diContainer.AddTag(window); CommonStartupAfterWindow(diContainer); + diContainer.AddTag(CreateConfiguration(diContainer)); var graphicsDevice = diContainer.GetTag(); var windowContainer = new WindowContainer(window, graphicsDevice); var openDocumentSet = new OpenDocumentSet(diContainer); @@ -151,7 +152,7 @@ private static void HandleInDev(InvocationContext ctx) if (time.HasFramerateChanged) window.Title = $"Zanzarah | {graphicsDevice.BackendType} | {time.FormattedStats}"; - assetRegistry.ApplyAssets(); + assetRegistry.Update(); using (remotery.SampleCPU("WindowContainer.Render")) { windowContainer.Render(); @@ -161,7 +162,7 @@ private static void HandleInDev(InvocationContext ctx) sdl.PumpEvents(); configuration.ApplyChanges(); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); windowContainer.BeginEventUpdate(time); Event ev = default; while (window.IsOpen && sdl.PollEvent(ref ev) != 0) diff --git a/zzre/Program.Logging.cs b/zzre/Program.Logging.cs index 0ef52338..469ee071 100644 --- a/zzre/Program.Logging.cs +++ b/zzre/Program.Logging.cs @@ -8,7 +8,7 @@ namespace zzre; -partial class Program +internal partial class Program { private const string LoggingOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"; @@ -32,9 +32,11 @@ partial class Program private static readonly Option OptionLogOverrides = new( ["--log"], - () => [], + Array.Empty, "Overrides the minimum level for a single log source (use \"Source=Level\")"); + private static readonly object consoleLock = new(); + private static void AddLoggingOptions(RootCommand command) { OptionLogOverrides.AddValidator(r => TryParseLogOverride(r.Token?.Value, out _, out _)); @@ -48,7 +50,7 @@ private static ILogger CreateLogging(ITagContainer diContainer, ILogEventSink? a var ctx = diContainer.GetTag(); var config = new LoggerConfiguration() .MinimumLevel.Is(ctx.ParseResult.GetValueForOption(OptionLogLevel)) - .WriteTo.Async(wt => wt.Console(outputTemplate: LoggingOutputTemplate)); + .WriteTo.Async(wt => wt.Console(outputTemplate: LoggingOutputTemplate, syncRoot: consoleLock)); var filePath = ctx.ParseResult.GetValueForOption(OptionLogFilePath); if (filePath is not null) diff --git a/zzre/Program.OpenAL.cs b/zzre/Program.OpenAL.cs index 7ac79cb4..54c03907 100644 --- a/zzre/Program.OpenAL.cs +++ b/zzre/Program.OpenAL.cs @@ -26,20 +26,12 @@ internal sealed class MojoALLibraryNameContainer : SearchPathContainer public static readonly MojoALLibraryNameContainer Instance = new(); } -internal unsafe sealed class OpenALDevice : BaseDisposable +internal sealed unsafe class OpenALDevice(ILogger logger, AL al, ALContext alc, Device* device) : BaseDisposable { - public ILogger Logger { get; } - public AL AL { get; } - public ALContext ALC { get; } - public Device* Device { get; private set; } - - public OpenALDevice(ILogger logger, AL al, ALContext alc, Device* device) - { - Logger = logger; - AL = al; - ALC = alc; - Device = device; - } + public ILogger Logger { get; } = logger; + public AL AL { get; } = al; + public ALContext ALC { get; } = alc; + public Device* Device { get; private set; } = device; protected override void DisposeNative() { @@ -54,7 +46,7 @@ protected override void DisposeNative() } } -unsafe partial class Program +internal unsafe partial class Program { private static readonly Option OptionNoSound = new( "--no-sound", diff --git a/zzre/Program.Remotery.cs b/zzre/Program.Remotery.cs index 61ac1cd4..01627efc 100644 --- a/zzre/Program.Remotery.cs +++ b/zzre/Program.Remotery.cs @@ -20,13 +20,6 @@ namespace zzre; -internal sealed class NullDisposable : IDisposable -{ - private NullDisposable() { } - public void Dispose() { } - public static readonly NullDisposable Instance = new(); -} - public sealed unsafe class Remotery : IDisposable, ILogEventSink { #if REMOTERY diff --git a/zzre/Program.Validation.cs b/zzre/Program.Validation.cs new file mode 100644 index 00000000..5566fcec --- /dev/null +++ b/zzre/Program.Validation.cs @@ -0,0 +1,81 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading; +using System.Threading.Tasks; +using zzre.validation; + +namespace zzre; + +internal partial class Program +{ + private static readonly Option OptionValidationConcurrency = new( + "--concurrency", + () => checked((ushort)Environment.ProcessorCount), + "Max number of parallel validation tasks" + ); + + private static void AddValidationCommand(RootCommand parent) + { + var command = new Command("validate", + "Starts an automated validation of assets, i.e. the capability of zzre to load and process them."); + command.AddOption(OptionValidationConcurrency); + command.SetHandler(HandleValidation); + parent.AddCommand(command); + } + + private static void HandleValidation(InvocationContext ctx) + { + var diContainer = CommonStartupBeforeWindow(ctx); + CommonStartupAfterWindow(diContainer); + + using var cancellationSource = new CancellationTokenSource(); + var validator = new Validator(diContainer) + { + MaxConcurrency = ctx.ParseResult.GetValueForOption(OptionValidationConcurrency) + }; + Task.Run(async () => + { + WriteConsoleLine("Validation: starting..."); + Console.CancelKeyPress += (_0, _1) => cancellationSource.Cancel(); + var validationTask = Task.Run(() => validator.Run(cancellationSource.Token)); + while (!validationTask.IsCompleted) + { + WriteProgress(); + await Task.Delay(500, cancellationSource.Token); + } + }, cancellationSource.Token).WaitAndRethrow(); + WriteProgress(); + + foreach (var diagnostic in validator.Diagnostics) + diagnostic.WriteToConsole(); + validator.LogSummary(); + + CommonCleanup(diContainer); + + void WriteProgress() => + WriteConsoleLine($"Validation: {validator.ProcessedFileCount:D8} processed / {validator.QueuedFileCount:D8} queued ({validator.FaultyFileCount} faulty)"); + + static void WriteConsoleLine(string line) + { + lock (consoleLock) + { + if (Console.IsErrorRedirected) + { + Console.Error.WriteLine(line); + return; + } + var (prevLeft, prevTop) = Console.GetCursorPosition(); + Console.SetCursorPosition(0, 0); + Console.Error.Write(line); + Console.Error.Write(new string(' ', Console.WindowWidth - line.Length)); + if (prevTop == 0) + { + prevLeft = 0; + prevTop = 1; + } + Console.SetCursorPosition(prevLeft, prevTop); + } + } + } +} diff --git a/zzre/Program.cs b/zzre/Program.cs index 89a4833d..c18b10c3 100644 --- a/zzre/Program.cs +++ b/zzre/Program.cs @@ -51,6 +51,7 @@ private static void Main(string[] args) AddGlobalRenderDocOption(rootCommand); AddRemoteryOptions(rootCommand); AddInDevCommand(rootCommand); + AddValidationCommand(rootCommand); rootCommand.Invoke(args); } @@ -70,9 +71,11 @@ private static ITagContainer CommonStartupBeforeWindow(InvocationContext ctx) private static void CommonStartupAfterWindow(ITagContainer diContainer) { - var window = diContainer.GetTag(); + if (diContainer.TryGetTag(out var window)) + SetupRenderDocKeys(window); + else + window = null; var ctx = diContainer.GetTag(); - SetupRenderDocKeys(window); var graphicsDevice = CreateGraphicsDevice(window, ctx); diContainer @@ -84,27 +87,28 @@ private static void CommonStartupAfterWindow(ITagContainer diContainer) ?? throw new InvalidDataException("Shader set is not compiled into zzre"))) .AddTag(new GameTime()) .AddTag(CreateResourcePool(diContainer)) - .AddTag(CreateConfiguration(diContainer)) .AddTag(CreateAssetRegistry(diContainer)); } - private static GraphicsDevice CreateGraphicsDevice(SdlWindow window, InvocationContext ctx) + private static GraphicsDevice CreateGraphicsDevice(SdlWindow? window, InvocationContext ctx) { var options = new GraphicsDeviceOptions() { Debug = ctx.ParseResult.GetValueForOption(OptionDebugLayers), - HasMainSwapchain = true, + HasMainSwapchain = window is not null, PreferDepthRangeZeroToOne = true, PreferStandardClipSpaceYDirection = true, SyncToVerticalBlank = true }; - SwapchainDescription scDesc = new SwapchainDescription( + if (window is null) + return GraphicsDevice.CreateVulkan(options); + SwapchainDescription scDesc = new( window.CreateSwapchainSource(), (uint)window.Width, (uint)window.Height, options.SwapchainDepthFormat, options.SyncToVerticalBlank, - colorSrgb : false); + colorSrgb: false); return GraphicsDevice.CreateVulkan(options, scDesc); } @@ -119,7 +123,7 @@ private static IResourcePool CreateResourcePool(ITagContainer diContainer) { 0 => new InMemoryResourcePool(), 1 => CreateSingleResourcePool(logger, pools.Single()), - _ => new CombinedResourcePool(pools.Select(p => CreateSingleResourcePool(logger, p)).ToArray()) + _ => new CombinedResourcePool([.. pools.Select(p => CreateSingleResourcePool(logger, p))]) }; } @@ -146,23 +150,8 @@ private static IAssetRegistry CreateAssetRegistry(ITagContainer diContainer) { var registryList = new AssetRegistryList(); diContainer.AddTag(registryList); - var registry = new AssetRegistry("", diContainer); + var registry = new AssetRegistry(diContainer, debugName: "Global"); registryList.Register("Global", registry); - SamplerAsset.Register(); - TextureAsset.Register(); - ClumpMaterialAsset.Register(); - ClumpAsset.Register(); - ActorMaterialAsset.Register(); - ActorAsset.Register(); - AnimationAsset.Register(); - WorldMaterialAsset.Register(); - WorldAsset.Register(); - EffectMaterialAsset.Register(); - EffectCombinerAsset.Register(); - UIBitmapAsset.Register(); - UITileSheetAsset.Register(); - UIPreloadAsset.Register(); - SoundAsset.Register(); return registry; } diff --git a/zzre/assets/ActorAsset.cs b/zzre/assets/ActorAsset.cs index fc7cafb4..b3e25bcd 100644 --- a/zzre/assets/ActorAsset.cs +++ b/zzre/assets/ActorAsset.cs @@ -1,83 +1,104 @@ using System; -using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using zzio; using zzio.vfs; namespace zzre; -public sealed class ActorAsset : Asset +public sealed class ActorAsset(IAssetRegistry registry, ActorAsset.Info info) : IAsset { private static readonly FilePath BasePath = new("resources/models/actorsex"); - private const AssetLoadPriority SecondaryPriority = AssetLoadPriority.High; public readonly record struct Info(string Name) { public FilePath FullPath => BasePath.Combine(Name + ".aed"); } - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); + private readonly record struct Part( + AssetHandle ClumpHandle, + AssetHandle[] AnimHandles) + { + public readonly ClumpAsset? Clump = ClumpHandle.Asset; + public readonly AnimationAsset[] Animations = [.. AnimHandles.Select(h => h.Asset ?? + throw new InvalidOperationException("Secondary asset was not loaded"))]; + + public void Dispose() + { + ClumpHandle.Dispose(); + foreach (var animHandle in AnimHandles ?? []) + animHandle.Dispose(); + } + } - private readonly Info info; - private ActorExDescription? description; - private AssetHandle[] bodyAnimations = []; - private AssetHandle[] wingsAnimations = []; + private readonly Info info = info; + private Part body, wings; + public IAssetRegistry Registry { get; } = registry; public string Name => info.Name; - public ActorExDescription Description => description ?? - throw new InvalidOperationException("Asset was not yet loaded"); - public AssetHandle Body { get; set; } = AssetHandle.Invalid; - public AssetHandle Wings { get; set; } = AssetHandle.Invalid; - public IReadOnlyList> BodyAnimations => bodyAnimations; - public IReadOnlyList> WingsAnimations => wingsAnimations; + public ActorExDescription Description { get; private init; } = null!; + public ClumpAsset Body => body.Clump!; + public ClumpAsset? Wings => wings.Clump; + public ReadOnlySpan BodyAnimations => body.Animations; + public ReadOnlySpan WingsAnimations => wings.Animations; - public ActorAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - this.info = info; - } + var resourcePool = registry.DIContainer.GetTag(); - protected override ValueTask> Load() - { - var resourcePool = diContainer.GetTag(); using var stream = resourcePool.FindAndOpen(info.FullPath) ?? throw new System.IO.FileNotFoundException($"Could not find actor: {info.Name}"); - description = ActorExDescription.ReadNew(stream); - (Body, bodyAnimations) = LoadSecondaryPart(description.body); + var description = ActorExDescription.ReadNew(stream); + var body = LoadSecondaryPart(registry, description.body); + var wings = new Part(default, []); if (description.HasWings) - (Wings, wingsAnimations) = LoadSecondaryPart(description.wings); + wings = LoadSecondaryPart(registry, description.wings); - var secondaryHandles = new AssetHandle[(description.HasWings ? 2 : 1) + bodyAnimations.Length + wingsAnimations.Length]; + var secondaryHandles = new Task[(description.HasWings ? 2 : 1) + body.Animations.Length + wings.Animations.Length]; var outI = 0; - AddSecondaryHandles(secondaryHandles, ref outI, Body, bodyAnimations); - if (description.HasWings) - AddSecondaryHandles(secondaryHandles, ref outI, Wings, wingsAnimations); - return ValueTask.FromResult>(secondaryHandles); + AddSecondaryHandles(secondaryHandles, ref outI, body, ct); + if (wings != default) + AddSecondaryHandles(secondaryHandles, ref outI, wings, ct); + await Task.WhenAll(secondaryHandles).WaitAsync(ct); + + return new AssetLoadResult(new ActorAsset(registry, info) + { + body = body, + wings = wings, + Description = description + }); } - private (AssetHandle, AssetHandle[]) LoadSecondaryPart(ActorPartDescription part) + private static Part LoadSecondaryPart(IAssetRegistry registry, ActorPartDescription part) { - var clump = Registry.Load(ClumpAsset.Info.Actor(part.model), SecondaryPriority).As(); + var clump = registry.LoadActorClump(part.model, AssetPriority.High); var animations = new AssetHandle[part.animations.Length]; for (int i = 0; i < animations.Length; i++) - animations[i] = Registry.Load(new AnimationAsset.Info(part.animations[i].filename), SecondaryPriority).As(); - return (clump, animations); + animations[i] = registry.LoadAnimation(part.animations[i].filename, AssetPriority.High); + return new(clump, animations); } - private static void AddSecondaryHandles(AssetHandle[] handles, ref int outI, AssetHandle clump, AssetHandle[] animations) + private static void AddSecondaryHandles(Task[] handles, ref int outI, in Part part, CancellationToken ct) { - handles[outI++] = clump; - for (int i = 0; i < animations.Length; i++) - handles[outI++] = animations[i]; + handles[outI++] = part.ClumpHandle.GetAsync(ct).AsTask(); + foreach (var animHandle in part.AnimHandles) + handles[outI++] = animHandle.GetAsync(ct).AsTask(); } - protected override void Unload() + public void Dispose() { - description = null; - bodyAnimations = []; - wingsAnimations = []; + body.Dispose(); + wings.Dispose(); + body = wings = default; } - protected override string ToStringInner() => $"Actor {info.Name}"; + public override string ToString() => $"Actor {info.Name}"; +} + +static partial class AssetExtensions +{ + public static AssetHandle LoadActor(this IAssetRegistry registry, string name, AssetPriority priority) => + registry.Load(new(name), priority); } diff --git a/zzre/assets/ActorMaterialAsset.cs b/zzre/assets/ActorMaterialAsset.cs index c31c76e3..afaf7776 100644 --- a/zzre/assets/ActorMaterialAsset.cs +++ b/zzre/assets/ActorMaterialAsset.cs @@ -1,5 +1,6 @@ using System; -using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Veldrid; using zzio; using zzio.rwbs; @@ -9,7 +10,7 @@ namespace zzre; -public sealed class ActorMaterialAsset : ModelMaterialAsset +public sealed class ActorMaterialAsset(IAssetRegistry registry) : IAsset { private static readonly FilePath[] ActorTextureBasePaths = [ @@ -17,52 +18,100 @@ public sealed class ActorMaterialAsset : ModelMaterialAsset new FilePath("resources/textures/models"), new FilePath("resources/textures/worlds"), ]; - protected override IReadOnlyList TextureBasePaths => ActorTextureBasePaths; + + // no factors, they are managed by ActorRenderer to set ambient light + + private static readonly ModelMaterial.Variant MaterialVariant = new( + IsInstanced: false, + IsSkinned: true, // TODO: comment why generally yes, but can differ + HasTexShift: false); + + static AssetLocality IAsset.Locality => AssetLocality.Unique; // due to skeleton buffers, no reuse possible + static bool IAsset.NeedsMainThreadDisposal => true; // an Apply action wants to access Material public readonly record struct Info( - string? textureName, - SamplerDescription sampler, - IColor color, - bool isSkinned, - StandardTextureKind? texturePlaceholder = null); + string? TextureName, + SamplerDescription Sampler, + IColor Color, + bool IsSkinned); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.SingleUsage); + private AssetHandle textureHandle; + private AssetHandle samplerHandle; - private readonly IColor color; - private readonly bool isSkinned; + public IAssetRegistry Registry { get; } = registry; + public ModelMaterial Material { get; private set; } = null!; - public ActorMaterialAsset(IAssetRegistry registry, Guid assetId, Info info) - : base(registry, assetId, info.textureName, info.sampler, info.texturePlaceholder) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { - color = info.color; - isSkinned = info.isSkinned; + var diContainer = registry.DIContainer; + var material = new ModelMaterial(diContainer) + { + DebugName = $"ActorMat {info.TextureName} {(info.IsSkinned ? "" : "Unskinned")}" + }; + material.Apply( + MaterialVariant with { IsSkinned = info.IsSkinned }, + factors: null, + diContainer + ); + material.Tint.Ref = info.Color; + + var camera = diContainer.GetTag(); + material.Projection.BufferRange = camera.ProjectionRange; + material.View.BufferRange = camera.ViewRange; + var samplerHandle = registry.LoadSampler(info.Sampler, AssetPriority.High); + material.Sampler.Sampler = (await samplerHandle.GetAsync(ct)).Sampler; + var (initialTexture, textureHandle) = await AssetExtensions.LoadTextureForMaterial( + registry, + ActorTextureBasePaths, + assetId, + info.TextureName, + StandardTextureKind.White, + AssetPriority.High, + ct); + material.Texture.Texture = initialTexture; + + return new(new ActorMaterialAsset(registry) + { + samplerHandle = samplerHandle, + textureHandle = textureHandle, + Material = material + }); } - protected override void SetMaterialVariant(ModelMaterial material) + public void Dispose() { - Material.Blend = ModelMaterial.BlendMode.Opaque; - Material.IsInstanced = false; - Material.IsSkinned = isSkinned; - Material.HasTexShift = false; - Material.HasFog = true; - Material.Tint.Ref = color; - if (diContainer.TryGetTag>(out var fogParams)) - Material.FogParams.Buffer = fogParams.Buffer; + textureHandle.Dispose(); + samplerHandle.Dispose(); + Material?.Dispose(); + Material = null!; } } partial class AssetExtensions { public static AssetHandle LoadActorMaterial(this IAssetRegistry registry, - RWMaterial rwMaterial, bool isSkinned) + string? textureName, + SamplerDescription sampler, + IColor color, + bool isSkinned = true, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load( + new(textureName, sampler, color, isSkinned), + priority); + + public static AssetHandle LoadActorMaterial(this IAssetRegistry registry, + RWMaterial rwMaterial, + bool isSkinned = true, + AssetPriority priority = AssetPriority.Synchronous) { var rwTexture = rwMaterial.FindChildById(SectionId.Texture, true) as RWTexture; var rwTextureName = (rwTexture?.FindChildById(SectionId.String, true) as RWString)?.value; var samplerDescription = GetSamplerDescription(rwTexture); - return registry.Load( - new Info(rwTextureName, samplerDescription, rwMaterial.color, isSkinned, StandardTextureKind.White), - AssetLoadPriority.Synchronous) - .As(); + return registry.LoadActorMaterial( + rwTextureName, + samplerDescription, + rwMaterial.color, + isSkinned, + priority); } } diff --git a/zzre/assets/AnimationAsset.cs b/zzre/assets/AnimationAsset.cs index 210f0136..e2a7cf2a 100644 --- a/zzre/assets/AnimationAsset.cs +++ b/zzre/assets/AnimationAsset.cs @@ -1,12 +1,12 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using zzio; using zzio.vfs; namespace zzre; -public sealed class AnimationAsset : Asset +public sealed class AnimationAsset : IAsset { private static readonly FilePath BasePath = new("resources/models/actorsex"); @@ -16,33 +16,34 @@ public readonly record struct Info(string Name) Name.EndsWith(".ska", StringComparison.OrdinalIgnoreCase) ? Name : Name + ".ska"); } - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); - private readonly Info info; - private SkeletalAnimation? animation; - public SkeletalAnimation Animation => animation ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public IAssetRegistry Registry { get; } + public SkeletalAnimation Animation { get; private set; } - public AnimationAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) + private AnimationAsset(IAssetRegistry registry, Info info, SkeletalAnimation animation) { this.info = info; + Registry = registry; + Animation = animation; } - protected override ValueTask> Load() + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - var resourcePool = diContainer.GetTag(); + var resourcePool = registry.DIContainer.GetTag(); using var stream = resourcePool.FindAndOpen(info.FullPath) ?? throw new System.IO.FileNotFoundException($"Could not find animation: {info.Name}"); - animation = SkeletalAnimation.ReadNew(stream); - return NoSecondaryAssets; + var animation = SkeletalAnimation.ReadNew(stream); + return Task.FromResult(new AssetLoadResult(new AnimationAsset(registry, info, animation))); } - protected override void Unload() - { - animation = null; - } + public void Dispose() { } - protected override string ToStringInner() => $"Animation {info.Name}"; + public override string ToString() => $"Animation {info.Name}"; +} + +static partial class AssetExtensions +{ + public static AssetHandle LoadAnimation(this IAssetRegistry registry, string name, AssetPriority priority) => + registry.Load(new(name), priority); } diff --git a/zzre/assets/ClumpAsset.cs b/zzre/assets/ClumpAsset.cs index e358d68d..ce2e915c 100644 --- a/zzre/assets/ClumpAsset.cs +++ b/zzre/assets/ClumpAsset.cs @@ -1,12 +1,12 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using zzio; using zzre.rendering; namespace zzre; -public sealed class ClumpAsset : Asset +public sealed class ClumpAsset : IAsset { private static readonly FilePath BasePath = new("resources/models/"); @@ -24,94 +24,42 @@ public Info(string directory, string name) : this(BasePath.Combine(directory, public string Name => FullPath.Parts[^1]; } - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); - private readonly Info info; - private ClumpMesh? mesh; public string Name => info.Name; - public ClumpMesh Mesh => mesh ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public IAssetRegistry Registry { get; } + public ClumpMesh Mesh { get; private set; } - public ClumpAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) + private ClumpAsset(IAssetRegistry registry, Info info, ClumpMesh mesh) { this.info = info; + Registry = registry; + Mesh = mesh; } - protected override ValueTask> Load() + public void Dispose() { - mesh = new ClumpMesh(diContainer, info.FullPath); - return NoSecondaryAssets; + Mesh.Dispose(); + Mesh = null!; } - protected override void Unload() + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - mesh?.Dispose(); - mesh = null; + var mesh = new ClumpMesh(registry.DIContainer, info.FullPath); + return Task.FromResult(new AssetLoadResult(new ClumpAsset(registry, info, mesh))); } - protected override string ToStringInner() => $"Clump {info.Name} ({info.Directory})"; + public override string ToString() => $"Clump {info.Name} ({info.Directory})"; } -public static unsafe partial class AssetExtensions +public static partial class AssetExtensions { - public static AssetHandle LoadBackdrop(this IAssetRegistry registry, - DefaultEcs.Entity entity, - string modelName, - AssetLoadPriority priority, - ClumpMaterialAsset.MaterialVariant? variant = null, - StandardTextureKind? texturePlaceholder = null) => - registry.LoadClump(entity, ClumpAsset.Info.Backdrop(modelName), priority, variant, texturePlaceholder); - - public static AssetHandle LoadModel(this IAssetRegistry registry, - DefaultEcs.Entity entity, - string modelName, - AssetLoadPriority priority, - ClumpMaterialAsset.MaterialVariant? variant = null, - StandardTextureKind? texturePlaceholder = null) => - registry.LoadClump(entity, ClumpAsset.Info.Model(modelName), priority, variant, texturePlaceholder); - - public static AssetHandle LoadClump(this IAssetRegistry registry, - DefaultEcs.Entity entity, - ClumpAsset.Info info, - AssetLoadPriority priority, - ClumpMaterialAsset.MaterialVariant? variant = null, - StandardTextureKind? texturePlaceholder = null) - { - var handle = variant.HasValue - ? registry.Load(info, priority, &ApplyClumpToEntityWithMaterials, (registry, entity, variant.Value, texturePlaceholder)) - : registry.Load(info, priority, &ApplyClumpToEntity, entity); - entity.Set(handle); - return handle.As(); - } + public static AssetHandle LoadModelClump(this IAssetRegistry registry, string name, AssetPriority priority) => + registry.Load(ClumpAsset.Info.Model(name), priority); - private static void ApplyClumpToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (!entity.IsAlive) - return; - var asset = handle.Get(); - entity.Set(asset.Mesh); - } + public static AssetHandle LoadActorClump(this IAssetRegistry registry, string name, AssetPriority priority) => + registry.Load(ClumpAsset.Info.Actor(name), priority); - private static void ApplyClumpToEntityWithMaterials(AssetHandle handle, - ref readonly (IAssetRegistry, DefaultEcs.Entity, ClumpMaterialAsset.MaterialVariant, StandardTextureKind?) context) - { - var (registry, entity, materialConfig, placeholder) = context; - if (!entity.IsAlive) - return; - var clumpMesh = handle.Get().Mesh; - entity.Set(clumpMesh); - - var materials = new List(clumpMesh.Materials.Count); - var handles = new AssetHandle[clumpMesh.Materials.Count]; - for (int i = 0; i < handles.Length; i++) - { - var materialHandle = registry.LoadClumpMaterial(clumpMesh.Materials[i], materialConfig, placeholder); - handles[i] = materialHandle; - materials.Add(materialHandle.Get().Material); - } - entity.Set(handles); - entity.Set(materials); - } + public static AssetHandle LoadBackdropClump(this IAssetRegistry registry, string name, AssetPriority priority) => + registry.Load(ClumpAsset.Info.Backdrop(name), priority); } diff --git a/zzre/assets/ClumpMaterialAsset.cs b/zzre/assets/ClumpMaterialAsset.cs index 41666bfe..3905a633 100644 --- a/zzre/assets/ClumpMaterialAsset.cs +++ b/zzre/assets/ClumpMaterialAsset.cs @@ -1,5 +1,6 @@ using System; -using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Veldrid; using zzio; using zzio.rwbs; @@ -9,7 +10,7 @@ namespace zzre; -public sealed class ClumpMaterialAsset : ModelMaterialAsset +public sealed class ClumpMaterialAsset(IAssetRegistry registry) : IAsset { private static readonly FilePath[] ClumpTextureBasePaths = [ @@ -17,65 +18,68 @@ public sealed class ClumpMaterialAsset : ModelMaterialAsset new FilePath("resources/textures/worlds"), new FilePath("resources/textures/backdrops") ]; - protected override IReadOnlyList TextureBasePaths => ClumpTextureBasePaths; + + private static readonly ModelFactors ModelFactors = new() + { + textureFactor = 1f, + vertexColorFactor = 1f, + tintFactor = 1f, + alphaReference = 0.082352944f + }; + + static AssetLocality IAsset.Locality => AssetLocality.Local; + static bool IAsset.NeedsMainThreadDisposal => true; // an Apply action wants to access Material public readonly record struct Info( - string? textureName, - SamplerDescription sampler, - MaterialVariant config, - StandardTextureKind? texturePlaceholder = null); + string? TextureName, + SamplerDescription Sampler, + ModelMaterial.Variant Variant, + StandardTextureKind? TexturePlaceholder = null); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + private AssetHandle textureHandle; + private AssetHandle samplerHandle; - public readonly record struct MaterialVariant( - ModelMaterial.BlendMode BlendMode = ModelMaterial.BlendMode.Opaque, - bool DepthWrite = true, - bool DepthTest = true, - bool HasEnvMap = false, - bool HasTexShift = true, - bool HasFog = true) - { - public MaterialVariant(zzio.effect.EffectPartRenderMode renderMode, bool depthTest) - : this(BlendFromRenderMode(renderMode), DepthWrite: false, depthTest, HasTexShift: false) { } + public IAssetRegistry Registry { get; } = registry; + public ModelMaterial Material { get; private set; } = null!; - private static ModelMaterial.BlendMode BlendFromRenderMode(zzio.effect.EffectPartRenderMode renderMode) => renderMode switch + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) + { + var diContainer = registry.DIContainer; + var material = new ModelMaterial(diContainer) { - zzio.effect.EffectPartRenderMode.Additive => ModelMaterial.BlendMode.Additive, - zzio.effect.EffectPartRenderMode.AdditiveAlpha => ModelMaterial.BlendMode.AdditiveAlpha, - zzio.effect.EffectPartRenderMode.NormalBlend => ModelMaterial.BlendMode.Alpha, - _ => throw new NotSupportedException($"Unsupported effect part render mode: {renderMode}") + DebugName = $"ClumpMat {info.TextureName} {info.Variant}" }; - } + material.Apply(info.Variant, ModelFactors, diContainer); - private readonly MaterialVariant materialVariant; + var camera = diContainer.GetTag(); + material.Projection.BufferRange = camera.ProjectionRange; + material.View.BufferRange = camera.ViewRange; + var samplerHandle = registry.LoadSampler(info.Sampler, AssetPriority.High); + material.Sampler.Sampler = (await samplerHandle.GetAsync(ct)).Sampler; + var (initialTexture, textureHandle) = await AssetExtensions.LoadTextureForMaterial( + registry, + ClumpTextureBasePaths, + assetId, + info.TextureName, + info.TexturePlaceholder, + AssetPriority.High, + ct); + material.Texture.Texture = initialTexture; - public ClumpMaterialAsset(IAssetRegistry registry, Guid assetId, Info info) - : base(registry, assetId, info.textureName, info.sampler, info.texturePlaceholder) - { - materialVariant = info.config; + return new(new ClumpMaterialAsset(registry) + { + samplerHandle = samplerHandle, + textureHandle = textureHandle, + Material = material + }); } - protected override void SetMaterialVariant(ModelMaterial material) + public void Dispose() { - Material.IsInstanced = true; - Material.IsSkinned = false; - Material.Blend = materialVariant.BlendMode; - Material.DepthWrite = materialVariant.DepthWrite; - Material.DepthTest = materialVariant.DepthTest; - Material.HasEnvMap = materialVariant.HasEnvMap; - Material.HasTexShift = materialVariant.HasTexShift; - Material.HasFog = materialVariant.HasFog; - - material.Factors.Ref = new() - { - textureFactor = 1f, - vertexColorFactor = 1f, - tintFactor = 1f, - alphaReference = 0.082352944f - }; - if (materialVariant.HasFog && diContainer.TryGetTag>(out var fogParams)) - material.FogParams.Buffer = fogParams.Buffer; + textureHandle.Dispose(); + samplerHandle.Dispose(); + Material?.Dispose(); + Material = null!; } } @@ -84,32 +88,28 @@ partial class AssetExtensions public static AssetHandle LoadClumpMaterial(this IAssetRegistry registry, string? textureName, SamplerDescription sampler, - MaterialVariant config, - StandardTextureKind? texturePlaceholder = null) => - registry.Load( - new Info(textureName, sampler, config, texturePlaceholder), - AssetLoadPriority.Synchronous) - .As(); - - public static AssetHandle LoadClumpMaterial(this IAssetRegistry registry, - StandardTextureKind texture, - MaterialVariant config) => - registry.Load( - new Info(null, SamplerDescription.Point, config, texture), - AssetLoadPriority.Synchronous) - .As(); + ModelMaterial.Variant config, + StandardTextureKind? texturePlaceholder = null, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load( + new(textureName, sampler, config, texturePlaceholder), + priority + ); public static AssetHandle LoadClumpMaterial(this IAssetRegistry registry, RWMaterial rwMaterial, - MaterialVariant config, - StandardTextureKind? texturePlaceholder = null) + ModelMaterial.Variant config, + StandardTextureKind? texturePlaceholder = null, + AssetPriority priority = AssetPriority.Synchronous) { var rwTexture = rwMaterial.FindChildById(SectionId.Texture, true) as RWTexture; var rwTextureName = (rwTexture?.FindChildById(SectionId.String, true) as RWString)?.value; var samplerDescription = GetSamplerDescription(rwTexture); - return registry.Load( - new Info(rwTextureName, samplerDescription, config, texturePlaceholder), - AssetLoadPriority.Synchronous) - .As(); + return registry.LoadClumpMaterial( + rwTextureName, + samplerDescription, + config, + texturePlaceholder, + priority); } } diff --git a/zzre/assets/CommonMaterialAsset.cs b/zzre/assets/CommonMaterialAsset.cs new file mode 100644 index 00000000..1368e021 --- /dev/null +++ b/zzre/assets/CommonMaterialAsset.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Veldrid; +using zzio; +using zzio.rwbs; +using zzre.rendering; + +namespace zzre; + +public interface ITexturedMaterialAsset : IAsset +{ + ITexturedMaterial Material { get; } +} + +partial class AssetExtensions +{ + internal static async Task<(Texture, AssetHandle)> LoadTextureForMaterial( + IAssetRegistry registry, + IReadOnlyList texturePaths, + Guid assetId, + string? textureName, + StandardTextureKind? placeholder, + AssetPriority priority, + CancellationToken ct) + { + var standardTextures = registry.DIContainer.GetTag(); + if (textureName is null && placeholder is null) + throw new ArgumentNullException(nameof(textureName), "Both textureName and placeholder are null"); + else if (textureName is null or "marker") // Funatics did not gave us this texture :( + { + return (standardTextures.ByKind(placeholder!.Value), default); + } + else if (placeholder is null) + { + var handle = registry.LoadTexture(texturePaths, textureName, priority); + var texture = await handle.GetAsync(ct); + return (texture.Texture, handle); + } + else + { + var handle = registry.LoadTexture(texturePaths, textureName, priority); + registry.Apply(handle, h => + { + if (registry.TryGet(assetId, out var materialHandle) && + materialHandle.Asset is ITexturedMaterialAsset { Material: ITexturedMaterial material }) + material.Texture.Texture = h.Asset?.Texture ?? material.Texture.Texture; + }); + return (standardTextures.ByKind(placeholder.Value), handle); + } + } + + private static SamplerDescription GetSamplerDescription(RWTexture? rwTexture) + { + if (rwTexture is null) + return SamplerDescription.Point; + var addressModeU = ConvertAddressMode(rwTexture.uAddressingMode); + return new() + { + AddressModeU = addressModeU, + AddressModeV = ConvertAddressMode(rwTexture.vAddressingMode, addressModeU), + Filter = ConvertFilterMode(rwTexture.filterMode), + MinimumLod = 0, + MaximumLod = 1000 // this should be VK_LOD_CLAMP_NONE + }; + } + + private static SamplerAddressMode ConvertAddressMode(TextureAddressingMode mode, SamplerAddressMode? altMode = null) => mode switch + { + TextureAddressingMode.Wrap => SamplerAddressMode.Wrap, + TextureAddressingMode.Mirror => SamplerAddressMode.Mirror, + TextureAddressingMode.Clamp => SamplerAddressMode.Clamp, + TextureAddressingMode.Border => SamplerAddressMode.Border, + + TextureAddressingMode.NATextureAddress => altMode ?? throw new NotImplementedException(), + TextureAddressingMode.Unknown => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + }; + + + private static SamplerFilter ConvertFilterMode(TextureFilterMode mode) => mode switch + { + TextureFilterMode.Nearest => SamplerFilter.MinPoint_MagPoint_MipPoint, + TextureFilterMode.Linear => SamplerFilter.MinLinear_MagLinear_MipPoint, + TextureFilterMode.MipNearest => SamplerFilter.MinPoint_MagPoint_MipPoint, + TextureFilterMode.MipLinear => SamplerFilter.MinLinear_MagLinear_MipPoint, + TextureFilterMode.LinearMipNearest => SamplerFilter.MinPoint_MagPoint_MipLinear, + TextureFilterMode.LinearMipLinear => SamplerFilter.MinLinear_MagLinear_MipLinear, + + TextureFilterMode.NAFilterMode => throw new NotImplementedException(), + TextureFilterMode.Unknown => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + }; +} diff --git a/zzre/assets/EffectCombinerAsset.cs b/zzre/assets/EffectCombinerAsset.cs index 225e03ac..0f796181 100644 --- a/zzre/assets/EffectCombinerAsset.cs +++ b/zzre/assets/EffectCombinerAsset.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using zzio; using zzio.effect; @@ -7,60 +7,43 @@ namespace zzre; -public sealed class EffectCombinerAsset : Asset -{ +public sealed class EffectCombinerAsset(IAssetRegistry registry, EffectCombinerAsset.Info info, EffectCombiner effect) : IAsset +{ + static AssetLocality IAsset.Locality => AssetLocality.Global; + public readonly record struct Info(FilePath FullPath) { public readonly string Name => FullPath.Parts[^1]; } - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); - - private readonly Info info; - private EffectCombiner? effectCombiner; - - public EffectCombiner EffectCombiner => effectCombiner ?? - throw new InvalidOperationException("Asset was not yet loaded"); - - public EffectCombinerAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) - { - this.info = info; - } + private readonly Info info = info; + public IAssetRegistry Registry { get; } = registry; + public EffectCombiner EffectCombiner { get; private set; } = effect; - protected override ValueTask> Load() + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { - var resourcePool = diContainer.GetTag(); + var resourcePool = registry.DIContainer.GetTag(); using var stream = resourcePool.FindAndOpen(info.FullPath) ?? throw new System.IO.FileNotFoundException($"Could not find effect combiner: {info.Name}"); - effectCombiner = new(); + var effectCombiner = new EffectCombiner(); effectCombiner.Read(stream); - return NoSecondaryAssets; + return Task.FromResult>(new( + new EffectCombinerAsset(registry, info, effectCombiner) + )); } - protected override void Unload() + public void Dispose() { - effectCombiner = null; + EffectCombiner = null!; } - protected override string ToStringInner() => $"EffectCombiner {info.Name}"; + public override string ToString() => $"EffectCombiner {info.Name}"; } partial class AssetExtensions { - public unsafe static AssetHandle LoadEffectCombiner(this IAssetRegistry registry, - DefaultEcs.Entity entity, + public static AssetHandle LoadEffectCombiner(this IAssetRegistry registry, FilePath fullPath, - AssetLoadPriority priority) - { - var handle = registry.Load(new EffectCombinerAsset.Info(fullPath), priority, &ApplyEffectCombinerToEntity, entity); - entity.Set(handle); - return handle.As(); - } - - private static void ApplyEffectCombinerToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (entity.IsAlive) - entity.Set(handle.Get().EffectCombiner); - } + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(new(fullPath), priority); } diff --git a/zzre/assets/EffectMaterialAsset.cs b/zzre/assets/EffectMaterialAsset.cs index 45c46647..b605a6f0 100644 --- a/zzre/assets/EffectMaterialAsset.cs +++ b/zzre/assets/EffectMaterialAsset.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Veldrid; using zzio; @@ -7,17 +6,22 @@ using zzre.materials; using zzre.rendering; using static zzre.materials.EffectMaterial; +using static zzre.EffectMaterialAsset; +using System.Threading; namespace zzre; -public sealed class EffectMaterialAsset : Asset +public sealed class EffectMaterialAsset(IAssetRegistry registry) : IAsset { - private static readonly FilePath[] TextureBasePaths = + private static readonly FilePath[] EffectTextureBasePaths = [ new("resources/textures/effects"), new("resources/textures/models") ]; + static AssetLocality IAsset.Locality => AssetLocality.Local; + static bool IAsset.NeedsMainThreadDisposal => true; // an Apply action wants to access Material + public readonly record struct Info( string TextureName, BillboardMode BillboardMode, @@ -27,115 +31,99 @@ public readonly record struct Info( float AlphaReference = 0.03f, StandardTextureKind TexturePlaceholder = StandardTextureKind.Clear); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); - - private readonly Info info; - private EffectMaterial? material; - - public string DebugName { get; } - public EffectMaterial Material => material ?? - throw new InvalidOperationException("Asset was not yet loaded"); - - public EffectMaterialAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) - { - this.info = info; - DebugName = $"{info.TextureName} {info.BillboardMode} {info.BlendMode}"; - if (!info.DepthTest) - DebugName += " NoDepthTest"; - } + private AssetHandle textureHandle; + private AssetHandle samplerHandle; - protected override bool NeedsSecondaryAssets => false; + public IAssetRegistry Registry { get; } = registry; + public EffectMaterial Material { get; private set; } = null!; - protected override ValueTask> Load() + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { + var diContainer = registry.DIContainer; diContainer.TryGetTag(out UniformBuffer fogParams); - material = new EffectMaterial(diContainer) + var material = new EffectMaterial(diContainer) { + DebugName = $"EffectMat {info.TextureName} {info.BillboardMode} {info.BlendMode} {(info.DepthTest ? "" : "NoDepthtest")}", DepthTest = info.DepthTest, Billboard = info.BillboardMode, Blend = info.BlendMode, - HasFog = info.HasFog && fogParams != null, - DebugName = DebugName + HasFog = info.HasFog && fogParams != null }; - - var camera = diContainer.GetTag(); - var samplerHandle = Registry.LoadSampler(SamplerDescription.Linear); - var textureHandle = LoadTexture(); - material.Sampler.Sampler = samplerHandle.Get().Sampler; - material.Projection.BufferRange = camera.ProjectionRange; - material.View.BufferRange = camera.ViewRange; - material.Factors.Value = new() + material.Factors.Ref = new() { alphaReference = info.AlphaReference }; - if (info.HasFog && fogParams is not null) - material.FogParams.Buffer = fogParams.Buffer; - - return textureHandle is null - ? ValueTask.FromResult>([ samplerHandle ]) - : ValueTask.FromResult>([ samplerHandle, textureHandle.Value ]); - } - private AssetHandle? LoadTexture() - { - if (material is null) - return null; - var standardTextures = diContainer.GetTag(); - var handle = Registry.LoadTexture( - TextureBasePaths, + var camera = diContainer.GetTag(); + material.Projection.BufferRange = camera.ProjectionRange; + material.View.BufferRange = camera.ViewRange; + var samplerHandle = registry.LoadSampler(SamplerDescription.Linear, AssetPriority.High); + material.Sampler.Sampler = (await samplerHandle.GetAsync(ct)).Sampler; + var (initialTexture, textureHandle) = await AssetExtensions.LoadTextureForMaterial( + registry, + EffectTextureBasePaths, + assetId, info.TextureName, - AssetLoadPriority.Low, - material); - material.Texture.Texture ??= standardTextures.ByKind(info.TexturePlaceholder); - return handle; + info.TexturePlaceholder, + AssetPriority.Low, + ct); + material.Texture.Texture = initialTexture; + + return new(new EffectMaterialAsset(registry) + { + samplerHandle = samplerHandle, + textureHandle = textureHandle, + Material = material + }); } - protected override void Unload() + public void Dispose() { - material?.Dispose(); - material = null; + textureHandle.Dispose(); + samplerHandle.Dispose(); + Material?.Dispose(); + Material = null!; } - - protected override string ToStringInner() => $"EffectMaterial {DebugName}"; } partial class AssetExtensions { public static AssetHandle LoadEffectMaterial(this IAssetRegistry registry, - DefaultEcs.Entity entity, string textureName, BillboardMode billboardMode, EffectPartRenderMode renderMode, - bool depthTest) => - registry.LoadEffectMaterial(entity, textureName, billboardMode, RenderToBlendMode(renderMode), depthTest); + bool depthTest, + AssetPriority priority = AssetPriority.Synchronous) => + registry.LoadEffectMaterial( + textureName, + billboardMode, + RenderToBlendMode(renderMode), + depthTest, + priority: priority); public static unsafe AssetHandle LoadEffectMaterial(this IAssetRegistry registry, - DefaultEcs.Entity entity, string TextureName, BillboardMode BillboardMode, BlendMode BlendMode, bool DepthTest, bool HasFog = true, float AlphaReference = 0.03f, - StandardTextureKind TexturePlaceholder = StandardTextureKind.Clear) => - registry.LoadEffectMaterial(entity, new EffectMaterialAsset.Info( - TextureName, BillboardMode, BlendMode, DepthTest, HasFog, AlphaReference, TexturePlaceholder)); + StandardTextureKind TexturePlaceholder = StandardTextureKind.Clear, + AssetPriority priority = AssetPriority.Synchronous) => + registry.LoadEffectMaterial(new Info( + TextureName, + BillboardMode, + BlendMode, + DepthTest, + HasFog, + AlphaReference, + TexturePlaceholder), + priority); public static unsafe AssetHandle LoadEffectMaterial(this IAssetRegistry registry, - DefaultEcs.Entity entity, - in EffectMaterialAsset.Info info) - { - var handle = registry.Load(info, AssetLoadPriority.Synchronous, &ApplyEffectMaterialToEntity, entity); - entity.Set(handle); - return handle.As(); - } - - private static void ApplyEffectMaterialToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (entity.IsAlive) - entity.Set(handle.Get().Material); - } + in Info info, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(info, priority); private static BlendMode RenderToBlendMode(EffectPartRenderMode renderMode) => renderMode switch { diff --git a/zzre/assets/ModelMaterialAsset.cs b/zzre/assets/ModelMaterialAsset.cs deleted file mode 100644 index 9a54e416..00000000 --- a/zzre/assets/ModelMaterialAsset.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Veldrid; -using zzio; -using zzio.rwbs; -using zzre.materials; -using zzre.rendering; - -namespace zzre; - -public abstract class ModelMaterialAsset : Asset -{ - private const string UseStandardTexture = "marker"; // Funatics never gave us this texture :( - - private readonly string? textureName; - private readonly SamplerDescription sampler; - private readonly StandardTextureKind? texturePlaceholder; - private ModelMaterial? material; - - public string DebugName { get; protected set; } - public ModelMaterial Material => material ?? - throw new InvalidOperationException("Asset was not yet loaded"); - - public ModelMaterialAsset(IAssetRegistry registry, Guid assetId, - string? textureName, - SamplerDescription sampler, - StandardTextureKind? texturePlaceholder) - : base(registry, assetId) - { - if (textureName is null && texturePlaceholder is null) - throw new ArgumentException("ClumpMaterialAsset cannot be loaded without a texture name and placeholder"); - this.textureName = textureName; - this.sampler = sampler; - this.texturePlaceholder = texturePlaceholder; - DebugName = $"{GetType().Name} {textureName ?? texturePlaceholder?.ToString()}"; - } - - protected override bool NeedsSecondaryAssets => false; - - protected override ValueTask> Load() - { - material = new ModelMaterial(diContainer); - material.DebugName = DebugName; - SetMaterialVariant(material); - - var camera = diContainer.GetTag(); - var samplerHandle = Registry.LoadSampler(sampler); - var textureHandle = LoadTexture(); - material.Sampler.Sampler = samplerHandle.Get().Sampler; - material.Projection.BufferRange = camera.ProjectionRange; - material.View.BufferRange = camera.ViewRange; - - return textureHandle is null - ? ValueTask.FromResult>([ samplerHandle ]) - : ValueTask.FromResult>([ samplerHandle, textureHandle.Value ]); - } - - private AssetHandle? LoadTexture() - { - if (material is null) - return null; - var standardTextures = diContainer.GetTag(); - if (textureName == UseStandardTexture) - { - material.Texture.Texture = standardTextures.ByKind(texturePlaceholder ?? StandardTextureKind.White); - return null; - } - else if (texturePlaceholder == null) - { - var handle = Registry.LoadTexture(TextureBasePaths, textureName!, AssetLoadPriority.Synchronous); - material.Texture.Texture = handle.Get().Texture; - return handle; - } - else - { - material.Texture.Texture = standardTextures.ByKind(texturePlaceholder.Value); - return textureName is null ? null - : Registry.LoadTexture(TextureBasePaths, textureName, AssetLoadPriority.High, material); - } - } - - protected override void Unload() - { - material?.Dispose(); - material = null; - } - - protected override string ToStringInner() => DebugName; - - protected abstract IReadOnlyList TextureBasePaths { get; } - protected abstract void SetMaterialVariant(ModelMaterial material); -} - -partial class AssetExtensions -{ - private static SamplerDescription GetSamplerDescription(RWTexture? rwTexture) - { - if (rwTexture is null) - return SamplerDescription.Point; - var addressModeU = ConvertAddressMode(rwTexture.uAddressingMode); - return new() - { - AddressModeU = addressModeU, - AddressModeV = ConvertAddressMode(rwTexture.vAddressingMode, addressModeU), - Filter = ConvertFilterMode(rwTexture.filterMode), - MinimumLod = 0, - MaximumLod = 1000 // this should be VK_LOD_CLAMP_NONE - }; - } - - private static SamplerAddressMode ConvertAddressMode(TextureAddressingMode mode, SamplerAddressMode? altMode = null) => mode switch - { - TextureAddressingMode.Wrap => SamplerAddressMode.Wrap, - TextureAddressingMode.Mirror => SamplerAddressMode.Mirror, - TextureAddressingMode.Clamp => SamplerAddressMode.Clamp, - TextureAddressingMode.Border => SamplerAddressMode.Border, - - TextureAddressingMode.NATextureAddress => altMode ?? throw new NotImplementedException(), - TextureAddressingMode.Unknown => throw new NotImplementedException(), - _ => throw new NotImplementedException(), - }; - - - private static SamplerFilter ConvertFilterMode(TextureFilterMode mode) => mode switch - { - TextureFilterMode.Nearest => SamplerFilter.MinPoint_MagPoint_MipPoint, - TextureFilterMode.Linear => SamplerFilter.MinLinear_MagLinear_MipPoint, - TextureFilterMode.MipNearest => SamplerFilter.MinPoint_MagPoint_MipPoint, - TextureFilterMode.MipLinear => SamplerFilter.MinLinear_MagLinear_MipPoint, - TextureFilterMode.LinearMipNearest => SamplerFilter.MinPoint_MagPoint_MipLinear, - TextureFilterMode.LinearMipLinear => SamplerFilter.MinLinear_MagLinear_MipLinear, - - TextureFilterMode.NAFilterMode => throw new NotImplementedException(), - TextureFilterMode.Unknown => throw new NotImplementedException(), - _ => throw new NotImplementedException(), - }; -} diff --git a/zzre/assets/SamplerAsset.cs b/zzre/assets/SamplerAsset.cs index 7c5a9b1a..cdb510c3 100644 --- a/zzre/assets/SamplerAsset.cs +++ b/zzre/assets/SamplerAsset.cs @@ -1,26 +1,34 @@ using System; -using System.Collections.Generic; using System.Text; +using System.Threading; using System.Threading.Tasks; using Veldrid; namespace zzre; -public sealed class SamplerAsset : Asset +public sealed class SamplerAsset(IAssetRegistry registry) : IAsset { - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); + public IAssetRegistry Registry { get; } = registry; + public string DebugName { get; private init; } = ""; + public Sampler Sampler { get; private set; } = null!; - private readonly SamplerDescription info; - private Sampler? sampler; - - public string DebugName { get; } - public Sampler Sampler => sampler ?? - throw new InvalidOperationException("Asset was not yet loaded"); + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, SamplerDescription info, CancellationToken ct) + { + var resourceFactory = registry.DIContainer.GetTag(); + var sampler = resourceFactory.CreateSampler(info); + var debugName = GetDebugName(info); + sampler.Name = debugName; + return Task.FromResult(new AssetLoadResult( + new SamplerAsset(registry) + { + DebugName = debugName, + Sampler = sampler + } + )); + } - public SamplerAsset(IAssetRegistry registry, Guid assetId, SamplerDescription info) : base(registry, assetId) + private static string GetDebugName(in SamplerDescription info) { - this.info = info; var stringBuilder = new StringBuilder("Sampler "); stringBuilder.Append(info.Filter); stringBuilder.Append(' '); @@ -32,28 +40,22 @@ public SamplerAsset(IAssetRegistry registry, Guid assetId, SamplerDescription in } if (info.MaximumLod == 0) stringBuilder.Append(" (No LOD)"); - DebugName = stringBuilder.ToString(); - } - - protected override ValueTask> Load() - { - var resourceFactory = diContainer.GetTag(); - sampler = resourceFactory.CreateSampler(info); - sampler.Name = DebugName; - return NoSecondaryAssets; + return stringBuilder.ToString(); } - protected override void Unload() + public void Dispose() { - sampler?.Dispose(); - sampler = null; + Sampler?.Dispose(); + Sampler = null!; } - protected override string ToStringInner() => DebugName; + public override string ToString() => DebugName; } -public static unsafe partial class AssetExtensions +static partial class AssetExtensions { - public static AssetHandle LoadSampler(this IAssetRegistry registry, in SamplerDescription info) => - registry.Load(info, AssetLoadPriority.Synchronous).As(); + public static AssetHandle LoadSampler(this IAssetRegistry registry, + in SamplerDescription info, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(info, priority); } diff --git a/zzre/assets/SoundAsset.cs b/zzre/assets/SoundAsset.cs index fe92d71e..f38f5f9d 100644 --- a/zzre/assets/SoundAsset.cs +++ b/zzre/assets/SoundAsset.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Silk.NET.OpenAL; using Silk.NET.OpenAL.Extensions.EXT; @@ -11,51 +11,47 @@ namespace zzre; -public sealed class SoundAsset : Asset +public sealed class SoundAsset(IAssetRegistry registry, SoundAsset.Info info) : IAsset { public readonly record struct Info(FilePath FullPath); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + static AssetLocality IAsset.Locality => AssetLocality.Local; + // As long as SoundContext locks the deferred buffer disposals, + // it is not necessary to set NeedsMainThreadDisposal - private readonly Info info; - private uint? buffer; + private readonly Info info = info; - public uint Buffer => buffer ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public IAssetRegistry Registry { get; } = registry; + public uint Buffer { get; private set; } - public SoundAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid __, Info info, CancellationToken ct) { - this.info = info; - } - - protected override ValueTask> Load() - { - if (!diContainer.TryGetTag(out OpenALDevice device) || - !diContainer.TryGetTag(out SoundContext context)) - { - buffer = 0; // A valid but unusable state - return NoSecondaryAssets; - } - - using var _ = context.EnsureIsCurrent(); - var ext = Path.GetExtension(info.FullPath.Parts[^1]).ToLowerInvariant(); - switch(ext) + var diContainer = registry.DIContainer; + uint buffer = 0; // a valid but unusable state + if (diContainer.TryGetTag(out OpenALDevice device) && + diContainer.TryGetTag(out SoundContext context)) { - case ".wav": LoadWave(device); break; - case ".mp3": LoadMP3(device); break; - default: throw new NotSupportedException($"Unsupported sound extension: {ext}"); + using var _ = context.EnsureIsCurrent(); + var ext = Path.GetExtension(info.FullPath.Parts[^1]).ToLowerInvariant(); + buffer = ext switch + { + ".wav" => LoadWave(diContainer, device, info), + ".mp3" => LoadMP3(diContainer, device, info), + _ => throw new NotSupportedException($"Unsupported sound extension: {ext}") + }; } - return NoSecondaryAssets; + return Task.FromResult(new AssetLoadResult( + new SoundAsset(registry, info) { Buffer = buffer } + )); } - private unsafe void LoadWave(OpenALDevice device) + private static unsafe uint LoadWave(ITagContainer diContainer, OpenALDevice device, in Info info) { var sdl = diContainer.GetTag(); var resourcePool = diContainer.GetTag(); var fileBuffer = resourcePool.FindAndRead(info.FullPath) ?? throw new FileNotFoundException("Could not open sound: " + info.FullPath); - FixTruncatedWave(ref fileBuffer); + FixTruncatedWave(diContainer, ref fileBuffer, info); var rwops = sdl.RWFromConstMem(fileBuffer); AudioSpec audioSpec = default; @@ -76,7 +72,7 @@ private unsafe void LoadWave(OpenALDevice device) _ => throw new NotSupportedException($"Unsupported audio format {audioSpec.Format}, {audioSpec.Channels} channels") }, audioBuf, (int)audioBufLen, audioSpec.Freq); device.AL.ThrowOnError(); - this.buffer = buffer; + return buffer; } finally { @@ -91,7 +87,7 @@ private unsafe void LoadWave(OpenALDevice device) private const uint FourCCfmt = 0x20746D66u; private const uint FourCCfact = 0x74636166; private const uint FourCCdata = 0x61746164; - private void FixTruncatedWave(ref byte[] original) + private static void FixTruncatedWave(ITagContainer diContainer, ref byte[] original, in Info info) { /* Some of the ADPCM encoded wave files in Zanzarah have truncated data blocks meaning * the data chunk size does not adhere to the reported alignment and the file might @@ -136,7 +132,7 @@ private void FixTruncatedWave(ref byte[] original) diContainer.GetLoggerFor().Verbose("Fixed truncated WAVE file (adding {Bytes} bytes): {Path}", newDataSize - dataSize, info.FullPath); } - private void LoadMP3(OpenALDevice device) + private static uint LoadMP3(ITagContainer diContainer, OpenALDevice device, in Info info) { var resourcePool = diContainer.GetTag(); using var stream = resourcePool.FindAndOpen(info.FullPath) ?? @@ -157,36 +153,23 @@ private void LoadMP3(OpenALDevice device) var buffer = device!.AL.GenBuffer(); device.AL.BufferData(buffer, format, samples, mpegFile.SampleRate); device.AL.ThrowOnError(); - this.buffer = buffer; + return buffer; } - protected override void Unload() + public void Dispose() { // We cannot just delete the buffer directly as the emitter disposal might not // have gone through yet, so we defer the disposal until later (usually next frame) - if (buffer is not (null or 0) && diContainer.TryGetTag(out SoundContext context)) - context.AddBufferDisposal(buffer.Value); - buffer = null; + if (Buffer is not 0 && Registry.DIContainer.TryGetTag(out SoundContext context)) + context.AddBufferDisposal(Buffer); + Buffer = 0; } - protected override string ToStringInner() => $"SoundAsset {info.FullPath}"; + public override string ToString() => $"SoundAsset {info.FullPath}"; } -partial class AssetExtensions +static partial class AssetExtensions { - public unsafe static AssetHandle LoadSound(this IAssetRegistry registry, - DefaultEcs.Entity entity, - FilePath path, - AssetLoadPriority priority) - { - var handle = registry.Load(new SoundAsset.Info(path), priority, &ApplySoundAssetToEntity, entity); - entity.Set(handle); - return handle.As(); - } - - private static void ApplySoundAssetToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (entity.IsAlive) - entity.Set(new game.components.SoundBuffer(handle.Get().Buffer)); - } + public static AssetHandle LoadSound(this IAssetRegistry registry, FilePath path, AssetPriority priority) => + registry.Load(new(path), priority); } diff --git a/zzre/assets/TextureAsset.cs b/zzre/assets/TextureAsset.cs index 174069cf..33dd5e59 100644 --- a/zzre/assets/TextureAsset.cs +++ b/zzre/assets/TextureAsset.cs @@ -1,53 +1,49 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Silk.NET.SDL; using Veldrid; using zzio; using zzio.vfs; +using zzio.rwbs; using zzre.rendering; using Texture = Veldrid.Texture; using PixelFormat = Veldrid.PixelFormat; namespace zzre; -public sealed class TextureAsset : Asset -{ public readonly record struct Info(FilePath FullPath) +public sealed class TextureAsset(IAssetRegistry registry, TextureAsset.Info info) : IAsset +{ + public readonly record struct Info(FilePath FullPath) { public Info(string fullPath) : this(new FilePath(fullPath)) { } } - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); - - private readonly FilePath path; - private Texture? texture; + private readonly Info info = info; - public Texture Texture => texture ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public IAssetRegistry Registry { get; } = registry; + public Texture Texture { get; private set; } = null!; - public TextureAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - path = info.FullPath; - } - - protected override ValueTask> Load() - { - var resourcePool = diContainer.GetTag(); - using var textureStream = resourcePool.FindAndOpen(path) ?? - throw new FileNotFoundException($"Could not open texture {path}"); - texture = (path.Extension ?? "").ToLowerInvariant() switch + var resourcePool = registry.DIContainer.GetTag(); + using var textureStream = resourcePool.FindAndOpen(info.FullPath) ?? + throw new FileNotFoundException($"Could not open texture {info.FullPath}"); + var texture = (info.FullPath.Extension ?? "").ToLowerInvariant() switch { - "dds" => LoadFromDDS(textureStream), - "bmp" => LoadFromBMP(textureStream), - _ => throw new NotSupportedException($"Unsupported texture extension: {path.Extension}") + "dds" => LoadFromDDS(registry.DIContainer, textureStream), + "bmp" => LoadFromBMP(registry.DIContainer, textureStream), + _ => throw new NotSupportedException($"Unsupported texture extension: {info.FullPath.Extension}") }; - texture.Name = path.Parts[^1]; - return NoSecondaryAssets; + texture.Name = info.FullPath.Parts[^1]; + return Task.FromResult(new AssetLoadResult( + new TextureAsset(registry, info) { Texture = texture } + )); } - private unsafe Texture LoadFromBMP(Stream textureStream) + private static unsafe Texture LoadFromBMP(ITagContainer diContainer, Stream textureStream) { var sdl = diContainer.GetTag(); var graphicsDevice = diContainer.GetTag(); @@ -74,7 +70,7 @@ private unsafe Texture LoadFromBMP(Stream textureStream) return image.ToTexture(graphicsDevice, "UNSET NAME"); } - private unsafe Texture LoadFromDDS(Stream stream) + private static unsafe Texture LoadFromDDS(ITagContainer diContainer, Stream stream) { using var image = Pfim.Dds.Create(stream, new Pfim.PfimConfig()); @@ -124,27 +120,25 @@ private unsafe Texture LoadFromDDS(Stream stream) _ => null }; - protected override void Unload() + public void Dispose() { - texture?.Dispose(); - texture = null; + Texture?.Dispose(); + Texture = null!; } - protected override string ToStringInner() => path.Parts.Count > 1 - ? $"Texture {path.Parts[^1]} ({path.Parts[^2]})" - : $"Texture {path.ToPOSIXString()}"; + public override string ToString() => info.FullPath.Parts.Count > 1 + ? $"Texture {info.FullPath.Parts[^1]} ({info.FullPath.Parts[^2]})" + : $"Texture {info.FullPath.ToPOSIXString()}"; } -public static unsafe partial class AssetExtensions +static partial class AssetExtensions { private static readonly IReadOnlyList TextureExtensions = [".dds", ".bmp"]; public static AssetHandle? TryLoadTexture(this IAssetRegistry registry, IReadOnlyList texturePaths, string textureName, - AssetLoadPriority priority, - ITexturedMaterial? material = null, - StandardTextureKind? placeholder = null) + AssetPriority priority) { var resourcePool = registry.DIContainer.GetTag(); foreach (var texturePath in texturePaths) @@ -153,33 +147,42 @@ public static unsafe partial class AssetExtensions { var path = texturePath.Combine(textureName + extension); if (resourcePool.FindFile(path) is not null) - return registry.LoadTexture(path, priority, material); + return registry.LoadTexture(path, priority); } } - if (material != null && placeholder != null) - material.Texture.Texture = registry.DIContainer.GetTag().ByKind(placeholder.Value); return null; } public static AssetHandle LoadTexture(this IAssetRegistry registry, IReadOnlyList texturePaths, string textureName, - AssetLoadPriority priority, - ITexturedMaterial? material = null) => - registry.TryLoadTexture(texturePaths, textureName, priority, material) ?? + AssetPriority priority) => + registry.TryLoadTexture(texturePaths, textureName, priority) ?? throw new FileNotFoundException($"Could not find any texture \"{textureName}\""); public static AssetHandle LoadTexture(this IAssetRegistry registry, FilePath fullPath, - AssetLoadPriority priority, - ITexturedMaterial? material = null) => material is null - ? registry.Load(new TextureAsset.Info(fullPath), priority).As() - : registry.Load(new TextureAsset.Info(fullPath), priority, &ApplyTextureToMaterial, material).As(); + AssetPriority priority) => + registry.Load(new(fullPath), priority); - private static void ApplyTextureToMaterial(AssetHandle handle, ref readonly ITexturedMaterial material) + public static AssetHandle? TryLoadTextureForMaterial(this IAssetRegistry registry, + IReadOnlyList texturePaths, + RWTexture rwTexture, + ITexturedMaterial material, + StandardTextureKind? placeholder = null) { - var texture = handle.Get().Texture; - if (!material.WasDisposed) - material.Texture.Texture = texture; + var rwTextureName = (RWString?)rwTexture.FindChildById(SectionId.String, true); + if (rwTextureName is not null) + { + var handle = registry.TryLoadTexture(texturePaths, rwTextureName.value, AssetPriority.Synchronous); + if (handle.HasValue) + { + material.Texture.Texture = handle.Value.Get().Texture; + return handle; + } + } + if (placeholder is not null && registry.DIContainer.TryGetTag(out var stdTextures)) + material.Texture.Texture = stdTextures.ByKind(placeholder.Value); + return null; } } diff --git a/zzre/assets/UIBitmapAsset.cs b/zzre/assets/UIBitmapAsset.cs index 30b02041..63866a99 100644 --- a/zzre/assets/UIBitmapAsset.cs +++ b/zzre/assets/UIBitmapAsset.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.IO; using System.Numerics; +using System.Threading; using System.Threading.Tasks; using Silk.NET.SDL; using Veldrid; @@ -13,7 +13,8 @@ namespace zzre; -public class UIBitmapAsset : Asset +public sealed class UIBitmapAsset(IAssetRegistry registry, UIBitmapAsset.Info info) + : IAsset { private const string UnmaskedSuffix = ".bmp"; private const string ColorSuffix = "T.bmp"; @@ -23,50 +24,35 @@ public class UIBitmapAsset : Asset public readonly record struct Info(string Name, bool HasRawMask = false); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + static AssetLocality IAsset.Locality => AssetLocality.Local; // we set the UI projection matrix - private readonly Info info; - protected UIMaterial? material; + private readonly Info info = info; + private AssetHandle samplerHandle; - public string DebugName { get; } - public UIMaterial Material => material ?? - throw new InvalidOperationException("Asset was not yet loaded"); - public Vector2 Size => material is null ? Vector2.Zero - : new Vector2(material.MainTexture.Texture!.Width, material.MainTexture.Texture!.Height); + public IAssetRegistry Registry => registry; + public UIMaterial Material { get; private set; } = null!; + public Vector2 Size => new(Material.MainTexture.Texture!.Width, Material.MainTexture.Texture!.Height); - public UIBitmapAsset(IAssetRegistry registry, Guid assetId, Info info) : this(registry, assetId, info, null) { } - public UIBitmapAsset(IAssetRegistry registry, Guid assetId, Info info, string? debugName) : base(registry, assetId) - { - this.info = info; - DebugName = debugName ?? ($"UIBitmap {info.Name}" + (info.HasRawMask ? "" : " (Raw mask)")); - } - - // strictly speaking this is a workaround: waiting on global secondary assets - // from local primary ones currently does not work and will throw an exception - // practically we do not need this functionality: - // - We do not wait for textures (Model/Effect materials) - // - We don't have to wait for samplers - protected override bool NeedsSecondaryAssets => false; - - protected override ValueTask> Load() + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { + var debugName = $"UIBitmap {info.Name}" + (info.HasRawMask ? "" : " (Raw mask)"); + var samplerAsset = registry.LoadSampler(SamplerDescription.Linear, AssetPriority.High); + var diContainer = registry.DIContainer; var graphicsDevice = diContainer.GetTag(); - using var bitmap = LoadMaskedBitmap(info.Name); - var texture = bitmap.ToTexture(graphicsDevice, DebugName); - var samplerAsset = Registry.LoadSampler(SamplerDescription.Linear); - material = new UIMaterial(diContainer) + using var bitmap = LoadMaskedBitmap(diContainer, info.Name); + var texture = bitmap.ToTexture(graphicsDevice, debugName); + var material = new UIMaterial(diContainer) { - DebugName = DebugName, + DebugName = debugName, HasMask = info.HasRawMask }; material.MainTexture.Texture = texture; - material.MainSampler.Sampler = samplerAsset.Get().Sampler; + material.MainSampler.Sampler = (await samplerAsset.GetAsync(ct)).Sampler; material.ScreenSize.Buffer = diContainer.GetTag().ProjectionBuffer; if (info.HasRawMask) { - var mask = LoadRawMask(bitmap.Width, bitmap.Height); + var mask = LoadRawMask(diContainer, info.Name, bitmap.Width, bitmap.Height); var maskTexture = graphicsDevice.ResourceFactory.CreateTexture(new( (uint)bitmap.Width, (uint)bitmap.Height, depth: 1, mipLevels: 1, arrayLayers: 1, @@ -77,19 +63,23 @@ protected override ValueTask> Load() material.MaskTexture.Texture = maskTexture; } - return ValueTask.FromResult>([ samplerAsset ]); + return new(new UIBitmapAsset(registry, info) + { + samplerHandle = samplerAsset, + Material = material + }); } - protected unsafe SdlSurfacePtr LoadMaskedBitmap(string name) + internal static unsafe SdlSurfacePtr LoadMaskedBitmap(ITagContainer diContainer, string name) { var sdl = diContainer.GetTag(); var resourcePool = diContainer.GetTag(); var bitmap = - LoadBitmap(name, UnmaskedSuffix, withAlpha: true) ?? - LoadBitmap(name, ColorSuffix, withAlpha: true) ?? + LoadBitmap(diContainer, name, UnmaskedSuffix, withAlpha: true) ?? + LoadBitmap(diContainer, name, ColorSuffix, withAlpha: true) ?? throw new FileNotFoundException($"Could not open bitmap {name}"); - using var maskBitmap = LoadBitmap(name, MaskSuffix, withAlpha: null); + using var maskBitmap = LoadBitmap(diContainer, name, MaskSuffix, withAlpha: null); if (maskBitmap == null) return bitmap; if (bitmap.Width != maskBitmap?.Width || bitmap.Height != maskBitmap?.Height) @@ -103,7 +93,7 @@ protected unsafe SdlSurfacePtr LoadMaskedBitmap(string name) return bitmap; } - private unsafe SdlSurfacePtr? LoadBitmap(string name, string suffix, bool? withAlpha) + private static unsafe SdlSurfacePtr? LoadBitmap(ITagContainer diContainer, string name, string suffix, bool? withAlpha) { var sdl = diContainer.GetTag(); var resourcePool = diContainer.GetTag(); @@ -139,10 +129,10 @@ protected unsafe SdlSurfacePtr LoadMaskedBitmap(string name) return new(sdl, rawPointer); } - private byte[] LoadRawMask(int width, int height) + private static byte[] LoadRawMask(ITagContainer diContainer, string name, int width, int height) { var resourcePool = diContainer.GetTag(); - var maskPath = BasePath.Combine(info.Name + RawMaskSuffix); + var maskPath = BasePath.Combine(name + RawMaskSuffix); using var stream = resourcePool.FindAndOpen(maskPath) ?? throw new FileNotFoundException($"Could not open mask {maskPath}"); if (stream.Length != width * height) @@ -152,35 +142,25 @@ private byte[] LoadRawMask(int width, int height) return mask; } - protected override void Unload() + public void Dispose() { // UIBitmap (and UITileSheet) contain their textures without secondary assets // that is because sharing textures across UI materials does not exist - material?.MainTexture.Texture?.Dispose(); - material?.MaskTexture.Texture?.Dispose(); - material?.Dispose(); - material = null; + Material?.MainTexture.Texture?.Dispose(); + Material?.MaskTexture.Texture?.Dispose(); + Material?.Dispose(); + Material = null!; + samplerHandle.Dispose(); } - protected override string ToStringInner() => DebugName; + public override string ToString() => Material.DebugName; } -partial class AssetExtensions +static partial class AssetExtensions { - public static unsafe AssetHandle LoadUIBitmap(this IAssetRegistry registry, - DefaultEcs.Entity entity, + public static AssetHandle LoadUIBitmap(this IAssetRegistry registry, string name, bool hasRawMask = false, - AssetLoadPriority priority = AssetLoadPriority.Synchronous) - { - var handle = registry.Load(new UIBitmapAsset.Info(name, hasRawMask), priority, &ApplyUIBitmapToEntity, entity); - entity.Set(handle); - return handle.As(); - } - - private static void ApplyUIBitmapToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (entity.IsAlive) - entity.Set(handle.Get().Material); - } + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(new(name, hasRawMask), priority); } diff --git a/zzre/assets/UIPreloadAsset.cs b/zzre/assets/UIPreloadAsset.cs index bc24bf03..fecbca70 100644 --- a/zzre/assets/UIPreloadAsset.cs +++ b/zzre/assets/UIPreloadAsset.cs @@ -1,5 +1,6 @@ using System; -using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace zzre; @@ -13,44 +14,46 @@ namespace zzre; // form circular dependencies (fnt000 -> fnt001 -> fnt000) // which cannot be loaded by the AssetRegistry. -public sealed class UIPreloadAsset : Asset +public sealed class UIPreloadAsset(IAssetRegistry registry) : IAsset { public static readonly UITileSheetAsset.Info - Btn000 = new("btn000", IsFont: false), - Btn001 = new("btn001", IsFont: false), - Btn002 = new("btn002", IsFont: false), - Sld000 = new("sld000", IsFont: false), - Tit000 = new("tit000", IsFont: false), - Cur000 = new("cur000", IsFont: false), - Dnd000 = new("dnd000", IsFont: false), - Wiz000 = new("wiz000", IsFont: false), - Itm000 = new("itm000", IsFont: false), - Spl000 = new("spl000", IsFont: false), - Lne000 = new("lne000", IsFont: false), - Fnt000 = new("fnt000", IsFont: true), - Fnt001 = new("fnt001", IsFont: true), - Fnt002 = new("fnt002", IsFont: true), - Fnt003 = new("fnt003", IsFont: true), - Fnt004 = new("fnt004", IsFont: true), - Fsp000 = new("fsp000", IsFont: true), - Inf000 = new("inf000", IsFont: false), - Log000 = new("log000", IsFont: false), - Cls000 = new("cls000", IsFont: false), - Cls001 = new("cls001", IsFont: false), - Map000 = new("map000", IsFont: false), - Swt000 = new("swt000", IsFont: false); + Btn000 = new("btn000"), + Btn001 = new("btn001"), + Btn002 = new("btn002"), + Sld000 = new("sld000"), + Tit000 = new("tit000"), + Cur000 = new("cur000"), + Dnd000 = new("dnd000"), + Wiz000 = new("wiz000"), + Itm000 = new("itm000"), + Spl000 = new("spl000"), + Lne000 = new("lne000"), + Fnt000 = new("fnt000", LineHeight: 14f, CharSpacing: 1f), + Fnt001 = new("fnt001", CharSpacing: 1f), + Fnt002 = new("fnt002", LineHeight: 17f, CharSpacing: 1f, LineOffset: 2f), + Fnt003 = new("fnt003", LineHeight: 14f, CharSpacing: 1f), + Fnt004 = new("fnt004", LineHeight: 14f, CharSpacing: 1f), + Fsp000 = new("fsp000", CharSpacing: 0f), + Inf000 = new("inf000"), + Log000 = new("log000"), + Cls000 = new("cls000"), + Cls001 = new("cls001"), + Map000 = new("map000"), + Swt000 = new("swt000"); public readonly record struct Info; - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + private AssetHandle[] handles = []; - public UIPreloadAsset(IAssetRegistry registry, Guid assetId, Info _) : base(registry, assetId) - { } + static AssetLocality IAsset.Locality => AssetLocality.Local; // the bitmaps are local + public IAssetRegistry Registry { get; } = registry; - protected override async ValueTask> Load() + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - AssetHandle[] allAssets = + AssetHandle Preload(out AssetHandle handle, UITileSheetAsset.Info info) => + handle = registry.LoadUITileSheet(info, AssetPriority.High); + + AssetHandle[] allAssets = [ Preload(out var btn000, Btn000), Preload(out var btn001, Btn001), @@ -63,11 +66,11 @@ protected override async ValueTask> Load() Preload(out var itm000, Itm000), Preload(out var spl000, Spl000), Preload(out var lne000, Lne000), - Preload(out var fnt000, Fnt000, lineHeight: 14f, charSpacing: 1f), - Preload(out var fnt001, Fnt001, charSpacing: 1f), - Preload(out var fnt002, Fnt002, lineHeight: 17f, charSpacing: 1f, lineOffset: 2f), - Preload(out var fnt003, Fnt003, lineHeight: 14f, charSpacing: 1f), - Preload(out var fnt004, Fnt004, lineHeight: 14f, charSpacing: 1f), + Preload(out var fnt000, Fnt000), + Preload(out var fnt001, Fnt001), + Preload(out var fnt002, Fnt002), + Preload(out var fnt003, Fnt003), + Preload(out var fnt004, Fnt004), Preload(out var fsp000, Fsp000), Preload(out var inf000, Inf000), Preload(out var log000, Log000), @@ -77,9 +80,9 @@ protected override async ValueTask> Load() Preload(out var swt000, Swt000) ]; - await Registry.WaitAsyncAll([fnt000, fnt001, fnt002, fnt003]); + await Task.WhenAll(allAssets.Select(h => h.GetAsync(ct).AsTask())).WaitAsync(ct); - await SetAlternatives(target: fnt000, + SetAlternatives(target: fnt000, fnt001, fnt002, fsp000, @@ -90,12 +93,12 @@ await SetAlternatives(target: fnt000, cls001, fnt004); - await SetAlternatives(target: fnt001, + SetAlternatives(target: fnt001, fnt002, inf000, fnt000); - await SetAlternatives(target: fnt002, + SetAlternatives(target: fnt002, fnt001, inf000, fsp000, @@ -103,7 +106,7 @@ await SetAlternatives(target: fnt002, cls000, cls001); - await SetAlternatives(target: fnt003, + SetAlternatives(target: fnt003, fnt001, fnt002, fsp000, @@ -114,41 +117,25 @@ await SetAlternatives(target: fnt003, cls001, fnt004); - return allAssets; - } - - protected override void Unload() - { } // we only have secondary assets - - private unsafe AssetHandle Preload(out AssetHandle handle, UITileSheetAsset.Info info, - float? lineHeight = null, - float? lineOffset = null, - float? charSpacing = null) - { - var applyConfig = (lineHeight ?? lineOffset ?? charSpacing) is not null; - return handle = (applyConfig - ? Registry.Load(info, AssetLoadPriority.High, &ApplyFontConfig, (lineHeight, lineOffset, charSpacing)) - : Registry.Load(info, AssetLoadPriority.High)) - .As(); + return new(new UIPreloadAsset(registry) + { + handles = allAssets + }); } - private static void ApplyFontConfig(AssetHandle handle, ref readonly (float?, float?, float?) config) + private static void SetAlternatives(AssetHandle target, params AssetHandle[] alternatives) { - var tileSheet = handle.Get().TileSheet; - var (lineHeight, lineOffset, charSpacing) = config; - if (lineHeight is not null) - tileSheet.LineHeight = lineHeight.Value; - if (lineOffset is not null) - tileSheet.LineOffset = lineOffset.Value; - if (charSpacing is not null) - tileSheet.CharSpacing = charSpacing.Value; + var targetTileSheet = target.Asset?.TileSheet + ?? throw new InvalidOperationException("Secondary asset was not loaded"); + foreach (var alternative in alternatives) + targetTileSheet.Alternatives.Add(alternative.Asset?.TileSheet + ?? throw new InvalidOperationException("Secondary asset was not loaded")); } - - private async Task SetAlternatives(AssetHandle target, params AssetHandle[] alternatives) + + public void Dispose() { - await Registry.WaitAsyncAll(alternatives); - var targetTileSheet = target.Get().TileSheet; - foreach (var alternative in alternatives) - targetTileSheet.Alternatives.Add(alternative.Get().TileSheet); + foreach (var handle in handles) + handle.Dispose(); + handles = []; } } diff --git a/zzre/assets/UITileSheetAsset.cs b/zzre/assets/UITileSheetAsset.cs index 5923ec9d..2acd6cfb 100644 --- a/zzre/assets/UITileSheetAsset.cs +++ b/zzre/assets/UITileSheetAsset.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Veldrid; using zzre.game; @@ -9,7 +9,7 @@ namespace zzre; -public sealed class UITileSheetAsset : UIBitmapAsset +public sealed class UITileSheetAsset(IAssetRegistry registry) : IAsset { private static readonly SamplerDescription SamplerDescription = new( SamplerAddressMode.Clamp, // the standard linear sampler uses Wrap @@ -19,30 +19,37 @@ public sealed class UITileSheetAsset : UIBitmapAsset comparisonKind: null, 0, 0, 0, 0, SamplerBorderColor.TransparentBlack); - public new readonly record struct Info(string Name, bool IsFont); + public readonly record struct Info( + string Name, + float? LineHeight = null, // set any of these to make this asset a font + float? LineOffset = null, + float? CharSpacing = null); - public new static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + static AssetLocality IAsset.Locality => AssetLocality.Local; - private readonly Info info; - private TileSheet? tileSheet; + private AssetHandle samplerHandle; - public TileSheet TileSheet => tileSheet ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public IAssetRegistry Registry => registry; + public UIMaterial Material { get; private set; } = null!; + public TileSheet TileSheet { get; private set; } = null!; - public UITileSheetAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId, new(info.Name), - $"UITileSheet {info.Name}" + (info.IsFont ? " (font)" : "")) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { - this.info = info; - } - - protected override ValueTask> Load() - { - using var bitmap = LoadMaskedBitmap(info.Name); - tileSheet = new TileSheet(info.Name, bitmap, info.IsFont); + var samplerHandle = registry.LoadSampler(SamplerDescription, AssetPriority.High); + var isFont = (info.LineHeight ?? info.LineOffset ?? info.CharSpacing) is not null; + var debugName = $"UITileSheet {info.Name}" + (isFont ? " (font)" : ""); + var diContainer = registry.DIContainer; + using var bitmap = UIBitmapAsset.LoadMaskedBitmap(diContainer, info.Name); + + var tileSheet = new TileSheet(info.Name, bitmap, isFont); + if (info.LineHeight is float lineHeight) + tileSheet.LineHeight = lineHeight; + if (info.LineOffset is float lineOffset) + tileSheet.LineOffset = lineOffset; + if (info.CharSpacing is float charSpacing) + tileSheet.CharSpacing = charSpacing; var graphicsDevice = diContainer.GetTag(); - var samplerHandle = Registry.LoadSampler(SamplerDescription); var texture = graphicsDevice.ResourceFactory.CreateTexture( new TextureDescription( (uint)bitmap.Width, @@ -53,63 +60,49 @@ protected override ValueTask> Load() PixelFormat.R8_G8_B8_A8_UNorm, TextureUsage.Sampled, TextureType.Texture2D)); - texture.Name = DebugName; - graphicsDevice.UpdateTexture(texture, bitmap.Data[(bitmap.Width * 4)..], - 0, 0, 0, width: texture.Width, height: texture.Height, depth: 1, mipLevel: 0, arrayLayer: 0); - - material = new UIMaterial(diContainer) + texture.Name = debugName; + graphicsDevice.UpdateTexture(texture, + bitmap.Data[(bitmap.Width * 4)..], + 0, 0, 0, + width: texture.Width, height: texture.Height, depth: 1, + mipLevel: 0, arrayLayer: 0); + + var material = new UIMaterial(diContainer) { - DebugName = DebugName, - IsFont = info.IsFont + DebugName = debugName, + IsFont = isFont }; material.MainTexture.Texture = texture; - material.MainSampler.Sampler = samplerHandle.Get().Sampler; + material.MainSampler.Sampler = (await samplerHandle.GetAsync(ct)).Sampler; material.ScreenSize.Buffer = diContainer.GetTag().ProjectionBuffer; - return ValueTask.FromResult>([ samplerHandle ]); + return new(new UITileSheetAsset(registry) + { + samplerHandle = samplerHandle, + Material = material, + TileSheet = tileSheet + }); } - protected override void Unload() + public void Dispose() { - base.Unload(); - tileSheet = null; + // UIBitmap (and UITileSheet) contain their textures without secondary assets + // that is because sharing textures across UI materials does not exist + Material?.MainTexture.Texture?.Dispose(); + Material?.MaskTexture.Texture?.Dispose(); + Material?.Dispose(); + Material = null!; + samplerHandle.Dispose(); + TileSheet = null!; } + + public override string ToString() => Material.DebugName; } -partial class AssetExtensions +static partial class AssetExtensions { - public static unsafe AssetHandle LoadUITileSheet(this IAssetRegistry registry, - DefaultEcs.Entity entity, - in UITileSheetAsset.Info info, - AssetLoadPriority priority = AssetLoadPriority.Synchronous) - { - var handle = registry.Load(info, priority, &ApplyUITileSheetToEntity, entity); - entity.Set(handle); - return handle.As(); - } - - private static void ApplyUITileSheetToEntity(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (entity.IsAlive) - { - var asset = handle.Get(); - entity.Set(asset.Material); - entity.Set(asset.TileSheet); - } - } - public static AssetHandle LoadUITileSheet(this IAssetRegistry registry, - in DefaultEcs.Command.EntityRecord entity, - in UITileSheetAsset.Info info) - { - // as the EntityRecord will be invalidated, - // loading tilesheets into a record can only be done synchronously - var handle = registry.Load(info, AssetLoadPriority.Synchronous); - entity.Set(handle); - - var asset = handle.Get(); - entity.Set(asset.Material); - entity.Set(asset.TileSheet); - return handle.As(); - } + in UITileSheetAsset.Info info, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(info, priority); } diff --git a/zzre/assets/WorldAsset.cs b/zzre/assets/WorldAsset.cs index 4d7540de..06db60b1 100644 --- a/zzre/assets/WorldAsset.cs +++ b/zzre/assets/WorldAsset.cs @@ -1,48 +1,42 @@ using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using zzio; using zzre.rendering; namespace zzre; -public sealed class WorldAsset : Asset +public sealed class WorldAsset(IAssetRegistry registry, WorldAsset.Info info, WorldMesh mesh) : IAsset { - public readonly record struct Info(FilePath FullPath); - - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Global); - - private readonly Info info; - private WorldMesh? mesh; + static AssetLocality IAsset.Locality => AssetLocality.Global; - public WorldMesh Mesh => mesh ?? - throw new InvalidOperationException("Asset was not yet loaded"); + public readonly record struct Info(FilePath FullPath); - public WorldAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) - { - this.info = info; - } + public IAssetRegistry Registry { get; } = registry; + public WorldMesh Mesh { get; private set; } = mesh; + private readonly Info info = info; - protected override ValueTask> Load() + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { - mesh = new WorldMesh(diContainer, info.FullPath); - return NoSecondaryAssets; + var mesh = new WorldMesh(registry.DIContainer, info.FullPath); + return Task.FromResult>(new( + new WorldAsset(registry, info, mesh) + )); } - protected override void Unload() + public void Dispose() { - mesh?.Dispose(); - mesh = null; + Mesh?.Dispose(); + Mesh = null!; } - protected override string ToStringInner() => $"World {info.FullPath.Parts[^1]}"; + public override string ToString() => $"World {info.FullPath.Parts[^1]}"; } -public static unsafe partial class AssetExtensions +static partial class AssetExtensions { public static AssetHandle LoadWorld(this IAssetRegistry registry, FilePath path, - AssetLoadPriority priority) => - registry.Load(new WorldAsset.Info(path), priority).As(); + AssetPriority priority) => + registry.Load(new(path), priority); } diff --git a/zzre/assets/WorldMaterialAsset.cs b/zzre/assets/WorldMaterialAsset.cs index fe9d9d97..5b9abcc9 100644 --- a/zzre/assets/WorldMaterialAsset.cs +++ b/zzre/assets/WorldMaterialAsset.cs @@ -1,5 +1,6 @@ using System; -using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Veldrid; using zzio; using zzio.rwbs; @@ -8,58 +9,96 @@ namespace zzre; -public sealed class WorldMaterialAsset : ModelMaterialAsset +public sealed class WorldMaterialAsset(IAssetRegistry registry) : IAsset { private static readonly FilePath[] WorldTextureBasePaths = [ new FilePath("resources/textures/worlds") ]; - protected override IReadOnlyList TextureBasePaths => WorldTextureBasePaths; + + private static readonly ModelMaterial.Variant MaterialVariant = new( + IsInstanced: false, + HasTexShift: false, + HasFog: true + ); + + private static readonly ModelFactors ModelFactors = new() + { + textureFactor = 1f, + vertexColorFactor = 1f, + tintFactor = 1f, + alphaReference = 0.6f + }; + + static AssetLocality IAsset.Locality => AssetLocality.Local; + static bool IAsset.NeedsMainThreadDisposal => true; // an Apply action wants to access Material public readonly record struct Info( - string? textureName, - SamplerDescription sampler); + string? TextureName, + SamplerDescription Sampler); - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + private AssetHandle textureHandle; + private AssetHandle samplerHandle; - public WorldMaterialAsset(IAssetRegistry registry, Guid assetId, Info info) - : base(registry, assetId, info.textureName, info.sampler, StandardTextureKind.White) - { - } + public IAssetRegistry Registry { get; } = registry; + public ModelMaterial Material { get; private set; } = null!; - protected override void SetMaterialVariant(ModelMaterial material) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { - Material.IsInstanced = false; - Material.IsSkinned = false; - Material.Blend = ModelMaterial.BlendMode.Opaque; - Material.HasTexShift = false; - Material.HasFog = diContainer.TryGetTag>(out var fogParams); - - material.Factors.Ref = new() + var diContainer = registry.DIContainer; + var material = new ModelMaterial(diContainer) { - textureFactor = 1f, - vertexColorFactor = 1f, - tintFactor = 1f, - alphaReference = 0.6f + DebugName = $"WorldMat {info.TextureName}" }; - if (fogParams is not null) - material.FogParams.Buffer = fogParams.Buffer; + material.Apply(MaterialVariant, ModelFactors, diContainer); + + var camera = diContainer.GetTag(); + material.Projection.BufferRange = camera.ProjectionRange; + material.View.BufferRange = camera.ViewRange; + var samplerHandle = registry.LoadSampler(info.Sampler, AssetPriority.High); + material.Sampler.Sampler = (await samplerHandle.GetAsync(ct)).Sampler; + var (initialTexture, textureHandle) = await AssetExtensions.LoadTextureForMaterial( + registry, + WorldTextureBasePaths, + assetId, + info.TextureName, + StandardTextureKind.White, + AssetPriority.High, + ct); + material.Texture.Texture = initialTexture; + + return new(new WorldMaterialAsset(registry) + { + samplerHandle = samplerHandle, + textureHandle = textureHandle, + Material = material + }); + } + + public void Dispose() + { + textureHandle.Dispose(); + samplerHandle.Dispose(); + Material?.Dispose(); + Material = null!; } } partial class AssetExtensions { + public static AssetHandle LoadWorldMaterial(this IAssetRegistry registry, + string? textureName, + SamplerDescription sampler, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load(new(textureName, sampler), priority); + public static AssetHandle LoadWorldMaterial(this IAssetRegistry registry, RWMaterial rwMaterial, - AssetLoadPriority priority) + AssetPriority priority = AssetPriority.Synchronous) { var rwTexture = rwMaterial.FindChildById(SectionId.Texture, true) as RWTexture; var rwTextureName = (rwTexture?.FindChildById(SectionId.String, true) as RWString)?.value; var samplerDescription = GetSamplerDescription(rwTexture); - return registry.Load( - new WorldMaterialAsset.Info(rwTextureName, samplerDescription), - priority) - .As(); + return registry.LoadWorldMaterial(rwTextureName, samplerDescription, priority); } } diff --git a/zzre/game/DuelGame.cs b/zzre/game/DuelGame.cs index d1bff4e8..de42fe5f 100644 --- a/zzre/game/DuelGame.cs +++ b/zzre/game/DuelGame.cs @@ -150,7 +150,7 @@ private DefaultEcs.Entity CreateFairyFor(DefaultEcs.Entity participant, Inventor ecsWorld.Publish(new messages.LoadActor( AsEntity: fairy, ActorName: dbRow.Mesh, - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); var actorParts = fairy.Get(); actorParts.Body.Get().Parent = fairy.Get(); diff --git a/zzre/game/EntityAssetExtensions.cs b/zzre/game/EntityAssetExtensions.cs new file mode 100644 index 00000000..f5e0b215 --- /dev/null +++ b/zzre/game/EntityAssetExtensions.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Numerics; +using zzre.materials; +using zzre.rendering; + +namespace zzre.game; + +public static class EntityAssetExtensions +{ + public static void SubscribeAt(this IAssetRegistry registry, DefaultEcs.World world) + { + world.SubscribeEntityComponentRemoved(HandleAssetHandleRemoved); + world.SubscribeEntityComponentRemoved(HandleAssetHandlesRemoved); + } + + private static void HandleAssetHandleRemoved(in DefaultEcs.Entity entity, in AssetHandle handle) => + handle.Dispose(); + + private static void HandleAssetHandlesRemoved(in DefaultEcs.Entity entity, in AssetHandle[] handles) + { + foreach (var handle in handles) + handle.Dispose(); + } + + public static Vector2 LoadUIBitmapFor(this IAssetRegistry registry, DefaultEcs.Entity entity, + string bitmap, bool hasRawMask = false) + { + var handle = registry.LoadUIBitmap(bitmap, hasRawMask); + var asset = handle.Get(); + entity.Set(handle.As()); + entity.Set(asset.Material); + return asset.Size; + } + + public static TileSheet LoadUITileSheetFor(this IAssetRegistry registry, DefaultEcs.Entity entity, + in UITileSheetAsset.Info info) + { + var handle = registry.LoadUITileSheet(info); + var asset = handle.Get(); + entity.Set(handle.As()); + entity.Set(asset.Material); + entity.Set(asset.TileSheet); + return asset.TileSheet; + } + + public static TileSheet LoadUITileSheetFor(this IAssetRegistry registry, ref DefaultEcs.Command.EntityRecord entity, + in UITileSheetAsset.Info info) + { + var handle = registry.LoadUITileSheet(info); + var asset = handle.Get(); + entity.Set(handle.As()); + entity.Set(asset.Material); + entity.Set(asset.TileSheet); + return asset.TileSheet; + } + + public static AssetHandle LoadModelClumpAndMaterialFor(this IAssetRegistry registry, DefaultEcs.Entity entity, + string modelName, + ModelMaterial.Variant variant, + StandardTextureKind placeholder, + AssetPriority priority) + { + var clumpHandle = registry.LoadModelClump(modelName, priority); + return LoadClumpMaterialFor(registry, entity, clumpHandle, variant, placeholder, priority); + } + + public static AssetHandle LoadBackdropClumpAndMaterialFor(this IAssetRegistry registry, DefaultEcs.Entity entity, + string modelName, + ModelMaterial.Variant variant, + StandardTextureKind placeholder, + AssetPriority priority) + { + var clumpHandle = registry.LoadBackdropClump(modelName, priority); + return LoadClumpMaterialFor(registry, entity, clumpHandle, variant, placeholder, priority); + } + + private static AssetHandle LoadClumpMaterialFor(IAssetRegistry registry, DefaultEcs.Entity entity, + AssetHandle clumpHandle, + ModelMaterial.Variant variant, + StandardTextureKind placeholder, + AssetPriority priority) + { + registry.Apply(clumpHandle, LoadMaterials); + entity.Set(clumpHandle.AsDuplicate()); + return clumpHandle; + + void LoadMaterials(AssetHandle clumpHandle) + { + if (!entity.IsAlive) + return; + var mesh = clumpHandle.Get().Mesh; + entity.Set(mesh); + + var materials = new List(mesh.Materials.Count); + var materialHandles = new AssetHandle[materials.Count]; + for (int i = 0; i < materials.Count; i++) + { + var materialHandle = registry.LoadClumpMaterial( + mesh.Materials[i], + variant, + placeholder, + AssetPriority.Synchronous); + materials.Add(materialHandle.Get().Material); + materialHandles[i] = materialHandle.As(); + } + entity.Set(materials); + entity.Set(materialHandles); + } + } +} diff --git a/zzre/game/Game.cs b/zzre/game/Game.cs index e938cea1..42479f93 100644 --- a/zzre/game/Game.cs +++ b/zzre/game/Game.cs @@ -14,7 +14,7 @@ public abstract class Game : BaseDisposable, ITagContainer { protected readonly ITagContainer tagContainer; protected readonly IZanzarahContainer zzContainer; - protected readonly AssetLocalRegistry assetRegistry; + protected readonly AssetRegistryDelayed assetRegistry; protected readonly ILogger logger; protected readonly Remotery profiler; protected readonly GameTime time; @@ -47,10 +47,11 @@ public Game(ITagContainer diContainer, Savegame savegame) profiler = GetTag(); time = GetTag(); ui = GetTag(); + var globalRegistry = GetTag(); AddTag(this); AddTag(savegame); - AddTag(assetRegistry = new AssetLocalRegistry("Game", tagContainer)); + AddTag(assetRegistry = new(new AssetRegistry(tagContainer, globalRegistry, "Game"))); AddTag(ecsWorld = new DefaultEcs.World()); AddTag(new LocationBuffer(GetTag(), 4096)); AddTag(new ModelInstanceBuffer(diContainer, 512)); // TODO: ModelRenderer should use central ModelInstanceBuffer @@ -66,7 +67,7 @@ public Game(ITagContainer diContainer, Savegame savegame) ecsWorld.Set(new Location()); // world location ecsWorld.Subscribe(diContainer.GetTag().Publish); // make sound a bit easier on us ecsWorld.Subscribe(DisposeUnusedAssets); - AssetRegistry.SubscribeAt(ecsWorld); + assetRegistry.SubscribeAt(ecsWorld); assetRegistry.DelayDisposals = true; } @@ -93,7 +94,7 @@ private void HandleResize() public void Update() { using var _ = profiler.SampleCPU("Game.Update"); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); onceUpdate.Invoke(); updateSystems.Update(time.Delta); } @@ -101,7 +102,7 @@ public void Update() public void Render(CommandList cl) { using var _ = profiler.SampleCPU("Game.Render"); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); camera.Update(cl); syncedLocation.Update(cl); cl.ClearColorTarget(0, clearColor); @@ -118,6 +119,7 @@ protected void LoadScene(string sceneName) ecsWorld.Publish(new messages.SceneChanging(sceneName)); ecsWorld.Publish(messages.LockPlayerControl.Unlock); // otherwise the timed entry locking will be ignored + // TODO: Make Scene a registerable asset type var resourcePool = GetTag(); SceneResource = resourcePool.FindFile($"resources/worlds/{sceneName}.scn") ?? throw new System.IO.FileNotFoundException($"Could not find scene: {sceneName}"); ; diff --git a/zzre/game/UI.cs b/zzre/game/UI.cs index 533cefa1..28e061a8 100644 --- a/zzre/game/UI.cs +++ b/zzre/game/UI.cs @@ -9,7 +9,7 @@ public class UI : BaseDisposable, ITagContainer { private readonly ITagContainer tagContainer; private readonly IZanzarahContainer zzContainer; - private readonly AssetLocalRegistry assetRegistry; + private readonly AssetRegistryDelayed assetRegistry; private readonly Remotery profiler; private readonly GameTime time; private readonly systems.RecordingSequentialSystem updateSystems; @@ -29,6 +29,7 @@ public UI(ITagContainer diContainer) zzContainer.OnResize += HandleResize; profiler = GetTag(); time = GetTag(); + var globalRegistry = GetTag(); var resourceFactory = GetTag(); graphicsDevice = GetTag(); @@ -39,13 +40,13 @@ public UI(ITagContainer diContainer) HandleResize(); AddTag(this); - AddTag(assetRegistry = new AssetLocalRegistry("UI", tagContainer)); + AddTag(assetRegistry = new(new AssetRegistry(tagContainer, globalRegistry, "UI"))); AddTag(World = new DefaultEcs.World()); AddTag(Builder = new UIBuilder(this)); if (TryGetTag(out tools.AssetRegistryList assetRegistryList)) assetRegistryList.Register("UI", assetRegistry); - AssetRegistry.SubscribeAt(World); + assetRegistry.SubscribeAt(World); assetRegistry.DelayDisposals = true; // we still use DelayDisposal in UI to prevent mostly sound samples to be freed @@ -108,14 +109,14 @@ protected override void DisposeManaged() public void Update() { using var _ = profiler.SampleCPU("UI.Update"); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); updateSystems.Update(time.Delta); } public void Render(CommandList cl) { using var _ = profiler.SampleCPU("UI.Render"); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); renderSystems.Update(cl); } diff --git a/zzre/game/components/sound/SoundBuffer.cs b/zzre/game/components/sound/SoundBuffer.cs deleted file mode 100644 index 7d87ef1e..00000000 --- a/zzre/game/components/sound/SoundBuffer.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace zzre.game.components; - -public readonly record struct SoundBuffer(uint Id); diff --git a/zzre/game/messages/LoadActor.cs b/zzre/game/messages/LoadActor.cs index 9f831303..8c11891d 100644 --- a/zzre/game/messages/LoadActor.cs +++ b/zzre/game/messages/LoadActor.cs @@ -3,4 +3,4 @@ public readonly record struct LoadActor( DefaultEcs.Entity AsEntity, string ActorName, - AssetLoadPriority Priority); + AssetPriority Priority); diff --git a/zzre/game/messages/LoadModel.cs b/zzre/game/messages/LoadModel.cs index 57dc409f..a0fb7bf0 100644 --- a/zzre/game/messages/LoadModel.cs +++ b/zzre/game/messages/LoadModel.cs @@ -8,12 +8,12 @@ public readonly record struct LoadModel( string ModelName, IColor Color, FOModelRenderType? RenderType = null, - AssetLoadPriority Priority = AssetLoadPriority.Synchronous) + AssetPriority Priority = AssetPriority.Synchronous) { public LoadModel( DefaultEcs.Entity AsEntity, string ModelName, FOModelRenderType? RenderType = null, - AssetLoadPriority Priority = AssetLoadPriority.Synchronous) + AssetPriority Priority = AssetPriority.Synchronous) : this(AsEntity, ModelName, IColor.White, RenderType, Priority) { } } \ No newline at end of file diff --git a/zzre/game/messages/sound/SpawnSample.cs b/zzre/game/messages/sound/SpawnSample.cs index 81645b4b..1b3d470c 100644 --- a/zzre/game/messages/sound/SpawnSample.cs +++ b/zzre/game/messages/sound/SpawnSample.cs @@ -12,7 +12,7 @@ public readonly record struct SpawnSample( bool Paused = false, Vector3? Position = null, Location? ParentLocation = null, - AssetLoadPriority Priority = AssetLoadPriority.Synchronous, + AssetPriority Priority = AssetPriority.Synchronous, bool IsMusic = false) { // a simple heuristic: If the caller cares about the entity, it probably needs the entity right away @@ -26,7 +26,7 @@ public SpawnSample( bool Paused = false, Vector3? Position = null, Location? ParentLocation = null, - AssetLoadPriority Priority = AssetLoadPriority.High) // otherwise we can take a bit more time to load the sound + AssetPriority Priority = AssetPriority.High) // otherwise we can take a bit more time to load the sound : this(SamplePath, AsEntity: null, RefDistance, MaxDistance, Volume, Looping, Paused, diff --git a/zzre/game/systems/WorldRendererSystem.cs b/zzre/game/systems/WorldRendererSystem.cs index d47d9d56..e6e6ce67 100644 --- a/zzre/game/systems/WorldRendererSystem.cs +++ b/zzre/game/systems/WorldRendererSystem.cs @@ -82,7 +82,7 @@ internal void LoadWorld(FilePath path) { DisposeWorld(); - worldAssetHandle = assetRegistry.LoadWorld(path, AssetLoadPriority.Synchronous); + worldAssetHandle = assetRegistry.LoadWorld(path, AssetPriority.Synchronous); worldMesh = worldAssetHandle.Get().Mesh; visibleMeshSections.EnsureCapacity(worldMesh.Sections.Count(s => s is WorldMesh.MeshSection)); visibleSubMeshes.EnsureCapacity(worldMesh.SubMeshes.Count); @@ -92,7 +92,7 @@ internal void LoadWorld(FilePath path) materials.EnsureCapacity(worldMesh.Materials.Count); foreach (var rwMaterial in worldMesh.Materials) { - var handle = assetRegistry.LoadWorldMaterial(rwMaterial, AssetLoadPriority.Synchronous); + var handle = assetRegistry.LoadWorldMaterial(rwMaterial, AssetPriority.Synchronous); materialAssetHandles.Add(handle); var material = handle.Get().Material; material.World.BufferRange = locationRange; diff --git a/zzre/game/systems/actor/ActorRenderer.cs b/zzre/game/systems/actor/ActorRenderer.cs index 77d7a45f..12d23485 100644 --- a/zzre/game/systems/actor/ActorRenderer.cs +++ b/zzre/game/systems/actor/ActorRenderer.cs @@ -130,64 +130,58 @@ private void HandleSceneLoaded(in messages.SceneLoaded msg) modelFactors.Ref.ambient = msg.Scene.misc.ambientLight.ToNumerics(); } - private unsafe void HandleLoadActor(in messages.LoadActor msg) + private void HandleLoadActor(in messages.LoadActor msg) { - var handle = assetRegistry.Load( - new ActorAsset.Info(msg.ActorName), - msg.Priority, - &ApplyActorToEntity, - (this, msg.AsEntity)); - msg.AsEntity.Set(handle); - } + var entity = msg.AsEntity; + var actorHandle = assetRegistry.LoadActor(msg.ActorName, msg.Priority); + assetRegistry.Apply(actorHandle, ApplyActorToEntity); + entity.Set(actorHandle.As()); - private static void ApplyActorToEntity(AssetHandle handle, - ref readonly (ActorRenderer thiz, DefaultEcs.Entity entity) context) - { - var (thiz, entity) = context; - if (!entity.IsAlive) - return; - var asset = handle.Get(); - entity.Set(asset.Description); - var body = thiz.CreateActorPart(entity, asset.Body, asset.BodyAnimations, asset.Description.body); - var wings = asset.Description.HasWings - ? thiz.CreateActorPart(entity, asset.Wings, asset.WingsAnimations, asset.Description.wings) - : null as DefaultEcs.Entity?; - entity.Set(new components.ActorParts(body, wings)); - - // attach to the "grandparent" as only animals are controlled directly by the entity - // (not meant as an insult by the way) - body.Get().Parent = entity.Get().Parent; - var bodySkeleton = body.Get(); - bodySkeleton.Location.Parent = body.Get(); - - if (wings is not null) + void ApplyActorToEntity(AssetHandle actorHandle) { - var wingsParentBone = bodySkeleton.Bones[asset.Description.attachWingsToBone]; - wings.Value.Get().Parent = wingsParentBone; + if (!entity.IsAlive) + return; + var asset = actorHandle.Get(); + entity.Set(asset.Description); + + var body = CreateActorPart(entity, asset.Body, asset.BodyAnimations, asset.Description.body); + var wings = asset.Wings is null ? null as DefaultEcs.Entity? + : CreateActorPart(entity, asset.Wings, asset.WingsAnimations, asset.Description.wings); + entity.Set(new components.ActorParts(body, wings)); + + // attach to the "grandparent" as only animals are controlled directly by the entity + // (not meant as an insult by the way) + body.Get().Parent = entity.Get().Parent; + var bodySkeleton = body.Get(); + bodySkeleton.Location.Parent = body.Get(); + + if (wings is not null) + { + var wingsParentBone = bodySkeleton.Bones[asset.Description.attachWingsToBone]; + wings.Value.Get().Parent = wingsParentBone; + } } } private DefaultEcs.Entity CreateActorPart( DefaultEcs.Entity parent, - AssetHandle clumpHandle, - IReadOnlyList> animations, + ClumpAsset clumpAsset, + ReadOnlySpan animations, ActorPartDescription partDescr) { - var clumpAsset = clumpHandle.Get(); var part = parent.World.CreateEntity(); part.Set(); part.Set(); part.Set(); part.Set(); part.Set(new components.Parent(parent)); - part.Set(clumpHandle); part.Set(clumpAsset.Mesh); if (clumpAsset.Mesh.Skin is not null) // unfortunately there are some unskinned actor parts part.Set(new Skeleton(clumpAsset.Mesh.Skin, clumpAsset.Name)); ref var animationPool = ref part.Get(); - for (int i = 0; i < animations.Count; i++) - animationPool.Add(partDescr.animations[i].type, animations[i].Get().Animation); + for (int i = 0; i < animations.Length; i++) + animationPool.Add(partDescr.animations[i].type, animations[i].Animation); LoadActorPartMaterials(part, clumpAsset.Mesh); @@ -202,12 +196,12 @@ private void LoadActorPartMaterials(DefaultEcs.Entity entity, ClumpMesh mesh) { entity.TryGet(out Skeleton skeleton); var materialHandle = assetRegistry.LoadActorMaterial(mesh.Materials[i], isSkinned: skeleton is not null); - handles[i] = materialHandle; var material = materials[i] = materialHandle.Get().Material; material.World.BufferRange = entity.Get().BufferRange; material.Factors.Buffer = modelFactors.Buffer; if (skeleton is not null) material.Pose.Skeleton = skeleton; + handles[i] = materialHandle.As(); } entity.Set(materials); entity.Set(handles); diff --git a/zzre/game/systems/animal/Animal.cs b/zzre/game/systems/animal/Animal.cs index da01412f..9d8885e6 100644 --- a/zzre/game/systems/animal/Animal.cs +++ b/zzre/game/systems/animal/Animal.cs @@ -71,7 +71,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) ecsWorld.Publish(new messages.LoadActor( AsEntity: entity, ActorName: actorFile, - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); var body = entity.Get().Body; body.Get().Parent = location; body.Get().JumpToAnimation( diff --git a/zzre/game/systems/animal/CollectionFairy.cs b/zzre/game/systems/animal/CollectionFairy.cs index 9ae4f721..9f986924 100644 --- a/zzre/game/systems/animal/CollectionFairy.cs +++ b/zzre/game/systems/animal/CollectionFairy.cs @@ -97,7 +97,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) World.Publish(new messages.LoadActor( AsEntity: entity, ActorName: dbRow.Mesh, - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); var actorParts = entity.Get(); actorParts.Body.Get().Parent = entity.Get(); diff --git a/zzre/game/systems/effect/BeamStar.cs b/zzre/game/systems/effect/BeamStar.cs index cb7d8135..87ccee54 100644 --- a/zzre/game/systems/effect/BeamStar.cs +++ b/zzre/game/systems/effect/BeamStar.cs @@ -29,11 +29,13 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi indexRange)); Reset(ref entity.Get(), data); - assetRegistry.LoadEffectMaterial(entity, + var materialHandle = assetRegistry.LoadEffectMaterial( data.texName, EffectMaterial.BillboardMode.None, data.renderMode, playback.DepthTest); + entity.Set(materialHandle.Get().Material); + entity.Set(materialHandle.As()); entity.Set(new components.effect.RenderIndices(indexRange)); } diff --git a/zzre/game/systems/effect/EffectCombiner.cs b/zzre/game/systems/effect/EffectCombiner.cs index bdc6ac25..fc44696f 100644 --- a/zzre/game/systems/effect/EffectCombiner.cs +++ b/zzre/game/systems/effect/EffectCombiner.cs @@ -74,8 +74,10 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) private void HandleSpawnEffect(in messages.SpawnEffectCombiner msg) { var entity = msg.AsEntity ?? World.CreateEntity(); - assetRegistry.LoadEffectCombiner(entity, msg.FullPath, AssetLoadPriority.Synchronous); - var effect = entity.Get(); + var effectHandle = assetRegistry.LoadEffectCombiner(msg.FullPath); + var effect = effectHandle.Get().EffectCombiner; + entity.Set(effectHandle.As()); + entity.Set(effect); entity.Set(new components.effect.CombinerPlayback( duration: effect.isLooping ? float.PositiveInfinity : effect.Duration, depthTest: msg.DepthTest)); diff --git a/zzre/game/systems/effect/LensFlare.cs b/zzre/game/systems/effect/LensFlare.cs index 5bcc20f3..a714c037 100644 --- a/zzre/game/systems/effect/LensFlare.cs +++ b/zzre/game/systems/effect/LensFlare.cs @@ -139,9 +139,11 @@ private void HandleSceneLoaded(in messages.SceneLoaded msg) { LocalPosition = trigger.pos }); - assetRegistry.LoadEffectMaterial(entity, MaterialInfo); + var materialHandle = assetRegistry.LoadEffectMaterial(MaterialInfo); var vertexRange = effectMesh.RentVertices(4 * FlareInfos[(int)trigger.ii1].Count); var indexRange = effectMesh.RentQuadIndices(vertexRange); + entity.Set(materialHandle.Get().Material); + entity.Set(materialHandle.As()); entity.Set(new components.effect.RenderIndices(indexRange)); entity.Set(components.RenderOrder.LateEffect); entity.Set(new components.effect.LensFlare( diff --git a/zzre/game/systems/effect/ModelEmitter.cs b/zzre/game/systems/effect/ModelEmitter.cs index 08f21e48..429171a3 100644 --- a/zzre/game/systems/effect/ModelEmitter.cs +++ b/zzre/game/systems/effect/ModelEmitter.cs @@ -44,11 +44,11 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi maxParticleCount)); entity.Set(modelInstanceBuffer.RentVertices(maxParticleCount)); - assetRegistry.LoadModel(entity, + using var _ = assetRegistry.LoadModelClumpAndMaterialFor(entity, data.texName, - AssetLoadPriority.Low, new(data.renderMode, playback.DepthTest), - StandardTextureKind.Clear); + StandardTextureKind.Clear, + AssetPriority.Low); entity.Set(); } diff --git a/zzre/game/systems/effect/MovingPlanes.cs b/zzre/game/systems/effect/MovingPlanes.cs index bc0c0b04..9dbfeb21 100644 --- a/zzre/game/systems/effect/MovingPlanes.cs +++ b/zzre/game/systems/effect/MovingPlanes.cs @@ -32,11 +32,13 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi }); Reset(ref entity.Get(), data); - assetRegistry.LoadEffectMaterial(entity, + var materialHandle = assetRegistry.LoadEffectMaterial( data.texName, isBillboard ? EffectMaterial.BillboardMode.View : EffectMaterial.BillboardMode.None, data.renderMode, playback.DepthTest); + entity.Set(materialHandle.Get().Material); + entity.Set(materialHandle.As()); entity.Set(new components.effect.RenderIndices(indexRange)); } diff --git a/zzre/game/systems/effect/ParticleEmitter.cs b/zzre/game/systems/effect/ParticleEmitter.cs index 7748cfd5..a516f90e 100644 --- a/zzre/game/systems/effect/ParticleEmitter.cs +++ b/zzre/game/systems/effect/ParticleEmitter.cs @@ -50,11 +50,13 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi vertexRange, indexRange)); - assetRegistry.LoadEffectMaterial(entity, + var materialHandle = assetRegistry.LoadEffectMaterial( data.texName, EffectMaterial.BillboardMode.View, data.renderMode, playback.DepthTest); + entity.Set(materialHandle.Get().Material); + entity.Set(materialHandle.As()); entity.Set(new components.effect.RenderIndices(default)); } diff --git a/zzre/game/systems/effect/RandomPlanes.cs b/zzre/game/systems/effect/RandomPlanes.cs index 96b48153..268cf651 100644 --- a/zzre/game/systems/effect/RandomPlanes.cs +++ b/zzre/game/systems/effect/RandomPlanes.cs @@ -48,11 +48,13 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi var billboardMode = data.circlesAround ? EffectMaterial.BillboardMode.None : EffectMaterial.BillboardMode.View; - assetRegistry.LoadEffectMaterial(entity, + var materialHandle = assetRegistry.LoadEffectMaterial( data.texName, billboardMode, data.renderMode, playback.DepthTest); + entity.Set(materialHandle.Get().Material); + entity.Set(materialHandle.As()); entity.Set(new components.effect.RenderIndices(default)); } diff --git a/zzre/game/systems/effect/Sound.cs b/zzre/game/systems/effect/Sound.cs index be276196..82bad646 100644 --- a/zzre/game/systems/effect/Sound.cs +++ b/zzre/game/systems/effect/Sound.cs @@ -33,7 +33,7 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi Paused: true, AsEntity: emitter, ParentLocation: parentLocation, - Priority: AssetLoadPriority.High)); + Priority: AssetPriority.High)); entity.Set(new components.effect.SoundState(emitter)); } diff --git a/zzre/game/systems/fairy/OverworldFairySpawner.cs b/zzre/game/systems/fairy/OverworldFairySpawner.cs index bbeda184..7a11a211 100644 --- a/zzre/game/systems/fairy/OverworldFairySpawner.cs +++ b/zzre/game/systems/fairy/OverworldFairySpawner.cs @@ -82,7 +82,7 @@ private void SpawnFairy(DefaultEcs.Entity parent, zzio.InventoryFairy invFairy) World.Publish(new messages.LoadActor( AsEntity: fairy, ActorName: dbRow.Mesh, - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); var actorParts = fairy.Get(); actorParts.Body.Get().Parent = fairy.Get(); diff --git a/zzre/game/systems/model/BackdropLoader.cs b/zzre/game/systems/model/BackdropLoader.cs index 1a5452ad..2bb7e901 100644 --- a/zzre/game/systems/model/BackdropLoader.cs +++ b/zzre/game/systems/model/BackdropLoader.cs @@ -77,12 +77,6 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) private DefaultEcs.Entity CreateStaticBackdrop(string name, bool depthTest = true, bool depthWrite = true, bool hasFog = true, Quaternion? rotation = null) { - var materialVariant = new ClumpMaterialAsset.MaterialVariant( - materials.ModelMaterial.BlendMode.Alpha, - DepthTest: depthTest, - DepthWrite: depthWrite, - HasFog: hasFog); - var entity = ecsWorld.CreateEntity(); entity.Set(new Location() { @@ -92,7 +86,14 @@ private DefaultEcs.Entity CreateStaticBackdrop(string name, bool depthTest = tru entity.Set(components.Visibility.Visible); entity.Set(components.RenderOrder.Backdrop); entity.Set(IColor.White); - assetRegistry.LoadBackdrop(entity, name, AssetLoadPriority.Synchronous, materialVariant, StandardTextureKind.Clear); + assetRegistry.LoadBackdropClumpAndMaterialFor(entity, name, + new( + materials.ModelMaterial.BlendMode.Alpha, + DepthTest: depthTest, + DepthWrite: depthWrite, + HasFog: hasFog), + StandardTextureKind.Clear, + AssetPriority.Synchronous); return entity; } diff --git a/zzre/game/systems/model/ModelLoader.cs b/zzre/game/systems/model/ModelLoader.cs index 285518d8..cd22ee30 100644 --- a/zzre/game/systems/model/ModelLoader.cs +++ b/zzre/game/systems/model/ModelLoader.cs @@ -88,8 +88,8 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) bool hasBehavior = behaviors.TryGetValue(model.idx, out var behaviour); var renderType = model.isVisualOnly ? FOModelRenderType.Solid : null as FOModelRenderType?; var priority = hasBehavior - ? AssetLoadPriority.Synchronous - : AssetLoadPriority.High; + ? AssetPriority.Synchronous + : AssetPriority.High; LoadModel(entity, model.filename, model.color, renderType, priority); if (hasBehavior) @@ -121,7 +121,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) }); SetPlantWiggle(entity, foModel.wiggleAmpl, plantWiggleDelay); - LoadModel(entity, foModel.filename, foModel.color, foModel.renderType, AssetLoadPriority.Low); + LoadModel(entity, foModel.filename, foModel.color, foModel.renderType, AssetPriority.Low); SetDistanceAlphaFade(entity, foModel); plantWiggleDelay++; @@ -131,9 +131,9 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) private void HandleLoadModel(in messages.LoadModel msg) => LoadModel(msg.AsEntity, msg.ModelName, msg.Color, msg.RenderType, msg.Priority); - private unsafe void LoadModel(DefaultEcs.Entity entity, string modelName, IColor color, FOModelRenderType? renderType, AssetLoadPriority priority = AssetLoadPriority.Synchronous) + private unsafe void LoadModel(DefaultEcs.Entity entity, string modelName, IColor color, FOModelRenderType? renderType, AssetPriority priority = AssetPriority.Synchronous) { - ClumpMaterialAsset.MaterialVariant material = renderType switch + ModelMaterial.Variant material = renderType switch { null => new(ModelMaterial.BlendMode.Opaque), FOModelRenderType.EarlySolid or FOModelRenderType.LateSolid or FOModelRenderType.Solid => @@ -156,18 +156,20 @@ FOModelRenderType.EnvMap196 or entity.Set(RenderOrderFromRenderType(renderType)); entity.Set(color with { a = AlphaFromRenderType(renderType) }); entity.Set(ClumpAsset.Info.Model(modelName)); - var handle = assetRegistry.LoadModel(entity, modelName, priority, material, StandardTextureKind.White); - handle.Inner.Apply(&ApplyModelAfterLoading, entity); - } - - private static void ApplyModelAfterLoading(AssetHandle handle, ref readonly DefaultEcs.Entity entity) - { - if (!entity.IsAlive) - return; - if (HasEmptyMesh(entity)) - entity.Dispose(); // I am fine with ignoring empty FOModels - else - SetSphereCollider(entity); + using var handle = assetRegistry.LoadModelClumpAndMaterialFor(entity, + modelName, + material, + StandardTextureKind.White, + priority); + assetRegistry.Apply(handle, handle => + { + if (!entity.IsAlive) + return; + if (HasEmptyMesh(entity)) + entity.Dispose(); // I am fine with ignoring empty FOModels + else + SetSphereCollider(entity); + }); } private void HandleCreateItem(in messages.CreateItem msg) @@ -185,7 +187,7 @@ private void HandleCreateItem(in messages.CreateItem msg) LocalRotation = Vector3.UnitX.ToZZRotation() }); SetBehaviour(entity, BehaviourType.CollectablePhysics, uint.MaxValue); - LoadModel(entity, $"itm{msg.ItemId:D3}", IColor.White, FOModelRenderType.Solid, AssetLoadPriority.Synchronous); + LoadModel(entity, $"itm{msg.ItemId:D3}", IColor.White, FOModelRenderType.Solid, AssetPriority.Synchronous); } } diff --git a/zzre/game/systems/npc/NPCScript.cs b/zzre/game/systems/npc/NPCScript.cs index 386af8c5..4b05e623 100644 --- a/zzre/game/systems/npc/NPCScript.cs +++ b/zzre/game/systems/npc/NPCScript.cs @@ -77,7 +77,7 @@ private void SetModel(DefaultEcs.Entity entity, string name) World.Publish(new messages.LoadActor( AsEntity: entity, ActorName: name, - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); var actorParts = entity.Get(); var bodyClump = actorParts.Body.Get(); diff --git a/zzre/game/systems/player/PlayerSpawner.cs b/zzre/game/systems/player/PlayerSpawner.cs index 02889e6c..a2c5706d 100644 --- a/zzre/game/systems/player/PlayerSpawner.cs +++ b/zzre/game/systems/player/PlayerSpawner.cs @@ -56,7 +56,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded message) ecsWorld.Publish(new messages.LoadActor( AsEntity: playerEntity, ActorName: "chr01", - AssetLoadPriority.Synchronous)); + AssetPriority.Synchronous)); playerEntity.Set(components.Visibility.Visible); playerEntity.Set(); playerEntity.Set(); diff --git a/zzre/game/systems/sound/AmbientSounds.cs b/zzre/game/systems/sound/AmbientSounds.cs index 0b6b816d..5eb7ce6a 100644 --- a/zzre/game/systems/sound/AmbientSounds.cs +++ b/zzre/game/systems/sound/AmbientSounds.cs @@ -180,7 +180,7 @@ public void Update(float _) MaxDistance: 20f, Position: position, AsEntity: landscapeEntities[i], - Priority: AssetLoadPriority.Low)); + Priority: AssetPriority.Low)); logger.Verbose("Spawned landscape sample {Sample} at relative {Position}", landscapeSamples[i], relativePosition); return; } diff --git a/zzre/game/systems/sound/SceneSamples.cs b/zzre/game/systems/sound/SceneSamples.cs index a8a06367..43b06721 100644 --- a/zzre/game/systems/sound/SceneSamples.cs +++ b/zzre/game/systems/sound/SceneSamples.cs @@ -68,7 +68,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded msg) Looping: sample.loopCount == 0, AsEntity: entity, Position: sample.pos, - Priority: AssetLoadPriority.Low)); + Priority: AssetPriority.Low)); samples.Add(entity); } } diff --git a/zzre/game/systems/sound/SoundEmitter.cs b/zzre/game/systems/sound/SoundEmitter.cs index a4560900..3653d78f 100644 --- a/zzre/game/systems/sound/SoundEmitter.cs +++ b/zzre/game/systems/sound/SoundEmitter.cs @@ -80,32 +80,36 @@ private unsafe void HandleSpawnSample(in messages.SpawnSample msg) Parent = msg.ParentLocation }); } - var handle = assetRegistry.LoadSound(entity, new zzio.FilePath(msg.SamplePath), msg.Priority); - handle.Inner.Apply(&ApplySpawnSample, (this, entity, msg)); + + var handle = assetRegistry.LoadSound(new zzio.FilePath(msg.SamplePath), msg.Priority); + if (msg.Priority is AssetPriority.Synchronous) + ApplySpawnSample(handle, entity, msg); + else + { + var msg_ = msg; + assetRegistry.Apply(handle, h => ApplySpawnSample(h, entity, msg_)); + } + entity.Set(handle.As()); } - private static void ApplySpawnSample(AssetHandle handle, - ref readonly (SoundEmitter, DefaultEcs.Entity, messages.SpawnSample) apply) + private void ApplySpawnSample(AssetHandle handle, DefaultEcs.Entity entity, messages.SpawnSample msg) { - var (thiz, entity, msg) = apply; if (!entity.IsAlive) return; - var context = thiz.context; - var device = thiz.device; using var _ = context.EnsureIsCurrent(); - if (!thiz.sourcePool.TryDequeue(out var sourceId)) + if (!sourcePool.TryDequeue(out var sourceId)) sourceId = device.AL.GenSource(); if (sourceId == 0) throw new InvalidOperationException("Source was not generated"); bool is3D = msg.Position.HasValue || msg.ParentLocation != null; - device.AL.SetSourceProperty(sourceId, SourceFloat.Gain, msg.Volume * thiz.gameConfig.SoundVolumeFactor); + device.AL.SetSourceProperty(sourceId, SourceFloat.Gain, msg.Volume * gameConfig.SoundVolumeFactor); device.AL.SetSourceProperty(sourceId, SourceFloat.ReferenceDistance, msg.RefDistance); device.AL.SetSourceProperty(sourceId, SourceFloat.MaxDistance, msg.MaxDistance); device.AL.SetSourceProperty(sourceId, SourceFloat.MinGain, 0f); device.AL.SetSourceProperty(sourceId, SourceFloat.MaxGain, 1f); device.AL.SetSourceProperty(sourceId, SourceFloat.RolloffFactor, is3D ? 1f : 0f); - device.AL.SetSourceProperty(sourceId, SourceInteger.Buffer, handle.Get().Buffer); + device.AL.SetSourceProperty(sourceId, SourceInteger.Buffer, handle.Get().Buffer); device.AL.SetSourceProperty(sourceId, SourceBoolean.Looping, msg.Looping); device.AL.SetSourceProperty(sourceId, SourceBoolean.SourceRelative, false); if (!is3D) diff --git a/zzre/game/systems/ui/Label.cs b/zzre/game/systems/ui/Label.cs index 2903249a..0c1c4b68 100644 --- a/zzre/game/systems/ui/Label.cs +++ b/zzre/game/systems/ui/Label.cs @@ -108,7 +108,7 @@ private void CreateSubLabel(DefaultEcs.Entity parent, IGrouping(); entity.Set(tiles.Select(t => t.tile).ToArray()); entity.Set(new components.Parent(parent)); - assetRegistry.LoadUITileSheet(entity, new(tiles.Key.Name, tiles.Key.IsFont)); + assetRegistry.LoadUITileSheetFor(ref entity, new(tiles.Key.Name, CharSpacing: tiles.Key.IsFont ? 0 : null)); } private static IReadOnlyList<(TileSheet tileSheet, components.ui.Tile tile)> FormatToTiles(in Rect rect, TileSheet rootTileSheet, string text, float lineHeight) diff --git a/zzre/game/systems/ui/ScrDeck.cs b/zzre/game/systems/ui/ScrDeck.cs index 72a37937..83e6672d 100644 --- a/zzre/game/systems/ui/ScrDeck.cs +++ b/zzre/game/systems/ui/ScrDeck.cs @@ -247,7 +247,7 @@ private DefaultEcs.Entity CreateSpellReq(DefaultEcs.Entity parent, SpellReq spel entity.Set(components.ui.UIOffset.Center); entity.Set(new components.ui.RenderOrder(renderOrder)); entity.Set(IColor.White); - assetRegistry.LoadUITileSheet(entity, isAttack ? UIPreloadAsset.Cls001 : UIPreloadAsset.Cls000); + assetRegistry.LoadUITileSheetFor(entity, isAttack ? UIPreloadAsset.Cls001 : UIPreloadAsset.Cls000); var tileSize = entity.Get().GetPixelSize(0); entity.Set(Rect.FromTopLeftSize(pos, tileSize * 3)); diff --git a/zzre/game/systems/ui/ScrMapMenu.cs b/zzre/game/systems/ui/ScrMapMenu.cs index c8996fa4..2eda342a 100644 --- a/zzre/game/systems/ui/ScrMapMenu.cs +++ b/zzre/game/systems/ui/ScrMapMenu.cs @@ -33,8 +33,8 @@ protected override void HandleOpen(in messages.ui.OpenMapMenu message) .With(new Rect(-320, -240, 640, 480)) .WithRenderOrder(1) .Build(); - var mapHandle = assetRegistry.LoadUIBitmap(mapEntity, "map001", hasRawMask: true); - mapHandle.Get().Material.MaskBits.Value = CollectMaskBits(inventory); + assetRegistry.LoadUIBitmapFor(mapEntity, "map001", hasRawMask: true); + entity.Get().MaskBits.Value = CollectMaskBits(inventory); preload.CreateTooltipTarget(entity) .With(new Vector2(-320 + 11, -240 + 11)) @@ -46,10 +46,10 @@ protected override void HandleOpen(in messages.ui.OpenMapMenu message) private static readonly IReadOnlyList MapSectionItems = new[] { - (StdItemId)(ushort.MaxValue), + (StdItemId)ushort.MaxValue, StdItemId.MapShadowRealm, StdItemId.MapFairyGarden, - (StdItemId)(ushort.MaxValue), + (StdItemId)ushort.MaxValue, StdItemId.MapSkyRealm, StdItemId.MapDarkSwamp, StdItemId.MapForest, diff --git a/zzre/game/uibuilder/ButtonLike.cs b/zzre/game/uibuilder/ButtonLike.cs index 2d9395ce..8d6eb525 100644 --- a/zzre/game/uibuilder/ButtonLike.cs +++ b/zzre/game/uibuilder/ButtonLike.cs @@ -41,7 +41,7 @@ protected override Entity BuildBase() throw new InvalidOperationException("Button-like UI element has no tile sheet"); var entity = base.BuildBase(); var assetRegistry = preload.UI.GetTag(); - assetRegistry.LoadUITileSheet(entity, tileSheet.Value); + assetRegistry.LoadUITileSheetFor(entity, tileSheet.Value); entity.Set(btnAlign); entity.Set(buttonTiles.Value); return entity; diff --git a/zzre/game/uibuilder/Image.cs b/zzre/game/uibuilder/Image.cs index 149ad8e1..0403d13b 100644 --- a/zzre/game/uibuilder/Image.cs +++ b/zzre/game/uibuilder/Image.cs @@ -61,13 +61,12 @@ public Entity Build() } else if (bitmap != null) { - var handle = assetRegistry.LoadUIBitmap(entity, bitmap); - size = handle.Get().Size; + size = assetRegistry.LoadUIBitmapFor(entity, bitmap); } else // if (tileSheet != null) { - var handle = assetRegistry.LoadUITileSheet(entity, tileSheet!.Value); - size = handle.Get().TileSheet.GetPixelSize(tileI); + var sheet = assetRegistry.LoadUITileSheetFor(entity, tileSheet!.Value); + size = sheet.GetPixelSize(tileI); } AlignToSize(entity, size, alignment); entity.Set(new components.ui.Tile[] { new(tileI, rect) }); diff --git a/zzre/game/uibuilder/LabelLike.cs b/zzre/game/uibuilder/LabelLike.cs index 225f2095..34e80940 100644 --- a/zzre/game/uibuilder/LabelLike.cs +++ b/zzre/game/uibuilder/LabelLike.cs @@ -79,8 +79,7 @@ protected override Entity BuildBase() throw new System.InvalidOperationException("Font was not set on label-like UI element"); var entity = base.BuildBase(); var assetRegistry = preload.UI.GetTag(); - var tileSheetHandle = assetRegistry.LoadUITileSheet(entity, font.Value); - var tileSheet = tileSheetHandle.Get().TileSheet; + var tileSheet = assetRegistry.LoadUITileSheetFor(entity, font.Value); if (lineHeight == null && useTotalFontHeight) lineHeight = tileSheet.TotalSize.Y; diff --git a/zzre/game/uibuilder/UIBuilder.cs b/zzre/game/uibuilder/UIBuilder.cs index e8ee2035..13253ef9 100644 --- a/zzre/game/uibuilder/UIBuilder.cs +++ b/zzre/game/uibuilder/UIBuilder.cs @@ -25,7 +25,7 @@ public UIBuilder(ITagContainer diContainer) mappedDb = diContainer.GetTag(); assetRegistry = diContainer.GetTag(); - preloadAssetHandle = assetRegistry.Load(new UIPreloadAsset.Info(), AssetLoadPriority.High).As(); + preloadAssetHandle = assetRegistry.Load(new(), AssetPriority.High); } protected override void DisposeManaged() diff --git a/zzre/materials/ModelMaterial.cs b/zzre/materials/ModelMaterial.cs index ae25de1e..fdd8082c 100644 --- a/zzre/materials/ModelMaterial.cs +++ b/zzre/materials/ModelMaterial.cs @@ -107,6 +107,60 @@ public ModelMaterial(ITagContainer diContainer) : base(diContainer, "model") DepthWrite = true; DepthTest = true; } + + public void Apply(in Variant variant, in ModelFactors? factors, ITagContainer diContainer) + { + if (!diContainer.TryGetTag(out UniformBuffer fogParams)) + fogParams = null!; + Apply(variant, factors, fogParams); + } + + public void Apply(in Variant variant, in ModelFactors? factors = null, UniformBuffer? fogParams = null) + { + IsInstanced = variant.IsInstanced; + IsSkinned = variant.IsSkinned; + Blend = variant.BlendMode; + DepthWrite = variant.DepthWrite; + DepthTest = variant.DepthTest; + HasEnvMap = variant.HasEnvMap; + HasTexShift = variant.HasTexShift; + + if (factors.HasValue) + Factors.Ref = factors.Value; + + if (variant.HasFog && fogParams is not null) + { + HasFog = true; + FogParams.Buffer = fogParams.Buffer; + } + } + + public readonly record struct Variant( + BlendMode BlendMode = BlendMode.Opaque, + bool DepthWrite = true, + bool DepthTest = true, + bool HasEnvMap = false, + bool HasTexShift = true, + bool HasFog = true, + bool IsInstanced = true, + bool IsSkinned = false) + { + public Variant(zzio.effect.EffectPartRenderMode renderMode, bool depthTest) + : this(BlendFromRenderMode(renderMode), DepthWrite: false, depthTest, HasTexShift: false) { } + + private static BlendMode BlendFromRenderMode(zzio.effect.EffectPartRenderMode renderMode) => renderMode switch + { + zzio.effect.EffectPartRenderMode.Additive => BlendMode.Additive, + zzio.effect.EffectPartRenderMode.AdditiveAlpha => BlendMode.AdditiveAlpha, + zzio.effect.EffectPartRenderMode.NormalBlend => BlendMode.Alpha, + _ => throw new NotSupportedException($"Unsupported effect part render mode: {renderMode}") + }; + + public override string ToString() => + $"{BlendMode} {Flag(!DepthWrite, "NoZWrite")} {Flag(!DepthTest, "NoZTest")} {Flag(HasEnvMap, "EnvMap")} {Flag(HasTexShift, "TexShift")} {Flag(!HasFog, "NoFog")}"; + + private static string Flag(bool enable, string value) => enable ? value : ""; + } } public sealed class ModelInstanceBuffer : DynamicMesh diff --git a/zzre/tools/ActorEditor.Part.cs b/zzre/tools/ActorEditor.Part.cs index 2f21efdb..1b021acb 100644 --- a/zzre/tools/ActorEditor.Part.cs +++ b/zzre/tools/ActorEditor.Part.cs @@ -41,11 +41,10 @@ public Part(ITagContainer diContainer, string modelName, (AnimationType type, st gameTime = diContainer.GetTag(); var resourcePool = diContainer.GetTag(); var assetRegistry = diContainer.GetTag(); - var modelPath = new FilePath("resources/models/actorsex/").Combine(modelName); - var meshHandle = assetRegistry.Load(new ClumpAsset.Info(modelPath), AssetLoadPriority.Synchronous); + var meshHandle = assetRegistry.LoadActorClump(modelName, AssetPriority.Synchronous); AddDisposable(meshHandle); - mesh = meshHandle.Get().Mesh; + mesh = meshHandle.Get().Mesh; locationBufferRange = diContainer.GetTag().Add(location); var camera = diContainer.GetTag(); @@ -68,9 +67,8 @@ void LinkTransformsFor(IStandardTransformMaterial material) { var material = materials[index] = new ModelMaterial(diContainer) { IsSkinned = skeleton != null }; var rwTexture = (RWTexture)rwMaterial.FindChildById(SectionId.Texture, true)!; - var rwTextureName = (RWString)rwTexture.FindChildById(SectionId.String, true)!; - var textureHandle = assetRegistry.TryLoadTexture([TextureBasePath], rwTextureName.value, - AssetLoadPriority.Synchronous, material, StandardTextureKind.Error); + var textureHandle = assetRegistry.TryLoadTextureForMaterial( + [TextureBasePath], rwTexture, material, StandardTextureKind.Error); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); if (textureHandle.HasValue) AddDisposable(textureHandle.Value); @@ -94,7 +92,7 @@ SkeletalAnimation LoadAnimation(string filename) throw new InvalidDataException($"Animation {filename} is incompatible with actor skeleton {modelName}"); return animation; } - animations = animationNames.Select(t => (t.type, t.filename, LoadAnimation(t.filename))).ToArray(); + animations = [.. animationNames.Select(t => (t.type, t.filename, LoadAnimation(t.filename)))]; skeleton?.ResetToBinding(); } diff --git a/zzre/tools/AssetExplorer.cs b/zzre/tools/AssetExplorer.cs index 962f8971..e9237e71 100644 --- a/zzre/tools/AssetExplorer.cs +++ b/zzre/tools/AssetExplorer.cs @@ -4,14 +4,14 @@ using zzre.imgui; using static ImGuiNET.ImGui; -using static zzre.IAssetRegistryDebug; +using static zzre.IAssetRegistry; namespace zzre.tools; internal sealed class AssetRegistryList { - private readonly List<(string, IAssetRegistryDebug)> registries = []; - public IReadOnlyList<(string Name, IAssetRegistryDebug Registry)> Registries + private readonly List<(string, IAssetRegistry)> registries = []; + public IReadOnlyList<(string Name, IAssetRegistry Registry)> Registries { get { @@ -19,7 +19,7 @@ internal sealed class AssetRegistryList return registries; } } - public void Register(string name, IAssetRegistryDebug registry) => registries.Add((name, registry)); + public void Register(string name, IAssetRegistry registry) => registries.Add((name, registry)); internal AssetExplorer? OpenExplorer { get; set; } } @@ -67,7 +67,8 @@ private void HandleContent() BeginTabBar("Registries", ImGuiTabBarFlags.None); foreach (var (name, registry) in registries) { - if (BeginTabItem($"{name} ({registry.LocalStats.Total})###{name}")) + var localTotal = registry.Stats.Total - (registry.ParentRegistry?.Stats.Total ?? 0); + if (BeginTabItem($"{name} ({localTotal})###{name}")) { HandleContentFor(registry); EndTabItem(); @@ -86,7 +87,7 @@ private enum Column Priority } - private void HandleContentFor(IAssetRegistryDebug registry) + private void HandleContentFor(IAssetRegistry registry) { registry.CopyDebugInfo(assets); if (!BeginTable("Assets", 6, @@ -122,7 +123,7 @@ private void HandleContentFor(IAssetRegistryDebug registry) if (TableSetColumnIndex(i++)) if (Selectable(asset.Name, selectedRow == asset.ID, ImGuiSelectableFlags.SpanAllColumns)) selectedRow = asset.ID; - if (TableSetColumnIndex(i++)) ImGuiEx.Text(asset.State); + if (TableSetColumnIndex(i++)) Text(asset.IsLoaded ? "Loaded" : "Not loaded"); if (TableSetColumnIndex(i++)) Text(asset.RefCount.ToString()); if (TableSetColumnIndex(i++)) ImGuiEx.Text(asset.Priority); if (selectedRow == asset.ID && focusSelectedRow) @@ -154,7 +155,7 @@ private unsafe void SortAssets() Column.Type => stringComparer.Compare(a.Type.Name, b.Type.Name), Column.Name => stringComparer.Compare(a.Name, b.Name), Column.RefCount => b.RefCount - a.RefCount, - Column.State => (int)b.State - (int)a.State, + Column.State => (b.IsLoaded ? 1 : 0) - (a.IsLoaded ? 1 : 0), Column.Priority => (int)b.Priority - (int)a.Priority, _ => 0 }; diff --git a/zzre/tools/ModelViewer.cs b/zzre/tools/ModelViewer.cs index 6fc46aff..28d80044 100644 --- a/zzre/tools/ModelViewer.cs +++ b/zzre/tools/ModelViewer.cs @@ -17,14 +17,14 @@ namespace zzre.tools; public class ModelViewer : ListDisposable, IDocumentEditor { - private const byte DebugPlaneAlpha = 0xA0; - - private enum CoarseCollisionMode - { - None, - BoundingBox, - BoundingSphere, - Both + private const byte DebugPlaneAlpha = 0xA0; + + private enum CoarseCollisionMode + { + None, + BoundingBox, + BoundingSphere, + Both } private readonly ITagContainer diContainer; @@ -37,20 +37,20 @@ private enum CoarseCollisionMode private readonly IResourcePool resourcePool; private readonly DebugLineRenderer gridRenderer; private readonly DebugLineRenderer triangleRenderer; - private readonly DebugLineRenderer normalRenderer; + private readonly DebugLineRenderer normalRenderer; private readonly DebugLineRenderer boundingRenderer; private readonly DebugPlaneRenderer planeRenderer; private readonly OpenFileModal openFileModal; private readonly ModelMaterialEdit modelMaterialEdit; private readonly LocationBuffer locationBuffer; - private readonly List assetHandles = []; + private readonly List assetHandles = []; private ClumpMesh? mesh; private GeometryCollider? collider; private ModelMaterial[] materials = []; private DebugSkeletonRenderer? skeletonRenderer; private int highlightedSplitI = -1; - private bool showNormals; + private bool showNormals; private CoarseCollisionMode coarseCollisionMode; public Window Window { get; } @@ -108,11 +108,11 @@ public ModelViewer(ITagContainer parentDiContainer) normalRenderer = new DebugLineRenderer(diContainer); normalRenderer.Material.LinkTransformsTo(camera); normalRenderer.Material.World.Ref = Matrix4x4.Identity; - AddDisposable(normalRenderer); - - boundingRenderer = new DebugLineRenderer(diContainer); - boundingRenderer.Material.LinkTransformsTo(camera); - boundingRenderer.Material.World.Ref = Matrix4x4.Identity; + AddDisposable(normalRenderer); + + boundingRenderer = new DebugLineRenderer(diContainer); + boundingRenderer.Material.LinkTransformsTo(camera); + boundingRenderer.Material.World.Ref = Matrix4x4.Identity; AddDisposable(boundingRenderer); planeRenderer = new DebugPlaneRenderer(diContainer); @@ -162,22 +162,23 @@ private void LoadModelNow(IResource resource) new FilePath("resources/textures/backdrops"), }; - normalRenderer.Clear(); - boundingRenderer.Clear(); + normalRenderer.Clear(); + boundingRenderer.Clear(); coarseCollisionMode = default; - var meshHandle = assetRegistry.Load(new ClumpAsset.Info(resource.Path), AssetLoadPriority.Synchronous); + var meshHandle = assetRegistry.Load(new(resource.Path), AssetPriority.Synchronous); assetHandles.Add(meshHandle); - mesh = meshHandle.Get().Mesh; + mesh = meshHandle.Get().Mesh; materials = new ModelMaterial[mesh.Materials.Count]; foreach (var (rwMaterial, index) in mesh.Materials.Indexed()) { var material = materials[index] = new ModelMaterial(diContainer); var rwTexture = (RWTexture)rwMaterial.FindChildById(SectionId.Texture, true)!; - var rwTextureName = (RWString)rwTexture.FindChildById(SectionId.String, true)!; - var textureHandle = assetRegistry.TryLoadTexture(texturePaths, rwTextureName.value, - AssetLoadPriority.Synchronous, material, StandardTextureKind.Error); + var textureHandle = assetRegistry.TryLoadTextureForMaterial(texturePaths, + rwTexture, + material, + StandardTextureKind.Error); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); if (textureHandle.HasValue) assetHandles.Add(textureHandle.Value); @@ -244,8 +245,8 @@ private void HandleRender(CommandList cl) if (normalRenderer.Count == 0) GenerateNormals(); normalRenderer.Render(cl); - } - if (coarseCollisionMode != default) + } + if (coarseCollisionMode != default) boundingRenderer.Render(cl); } @@ -360,22 +361,22 @@ private void SetPlanes(Box bounds, Vector3 normal, float leftValue, float rightV ]; } planeRenderer.Planes = planes; - } - - private void HandleCoarseCollisionContent() - { - if (mesh is null) - return; - var hasChanged = ImGuiEx.EnumCombo("Show coarse collision", ref coarseCollisionMode); - ImGui.NewLine(); - if (!hasChanged) - return; - boundingRenderer.Clear(); - if (coarseCollisionMode is CoarseCollisionMode.BoundingBox or CoarseCollisionMode.Both) - boundingRenderer.AddBox(mesh.BoundingBox, IColor.Blue); - if (coarseCollisionMode is CoarseCollisionMode.BoundingSphere or CoarseCollisionMode.Both) - boundingRenderer.AddDiamondSphere(mesh.BoundingSphere, IColor.Red); - fbArea.IsDirty = true; + } + + private void HandleCoarseCollisionContent() + { + if (mesh is null) + return; + var hasChanged = ImGuiEx.EnumCombo("Show coarse collision", ref coarseCollisionMode); + ImGui.NewLine(); + if (!hasChanged) + return; + boundingRenderer.Clear(); + if (coarseCollisionMode is CoarseCollisionMode.BoundingBox or CoarseCollisionMode.Both) + boundingRenderer.AddBox(mesh.BoundingBox, IColor.Blue); + if (coarseCollisionMode is CoarseCollisionMode.BoundingSphere or CoarseCollisionMode.Both) + boundingRenderer.AddDiamondSphere(mesh.BoundingSphere, IColor.Red); + fbArea.IsDirty = true; } private void HandleCollisionContent() @@ -384,7 +385,7 @@ private void HandleCollisionContent() { ImGui.Text("No model loaded"); return; - } + } HandleCoarseCollisionContent(); if (collider == null) { diff --git a/zzre/tools/WorldViewer.cs b/zzre/tools/WorldViewer.cs index 9bda224a..ea549386 100644 --- a/zzre/tools/WorldViewer.cs +++ b/zzre/tools/WorldViewer.cs @@ -12,6 +12,7 @@ using zzre.imgui; using zzre.materials; using zzre.rendering; +using zzre.game; using zzre.game.systems; using static ImGuiNET.ImGui; @@ -34,7 +35,7 @@ private enum IntersectionPrimitive private readonly ITagContainer localDiContainer; private readonly DefaultEcs.World ecsWorld; - private readonly AssetLocalRegistry assetRegistry; + private readonly IAssetRegistry assetRegistry; private readonly TwoColumnEditorTag editor; private readonly FlyControlsTag controls; private readonly FramebufferArea fbArea; @@ -49,8 +50,8 @@ private enum IntersectionPrimitive private readonly WorldRendererSystem worldRenderer; private readonly Camera camera; private readonly LocationBuffer locationBuffer; - private readonly UniformBuffer worldTransform; - private readonly List intersections = new List(128); // pooled to reduce GC + private readonly UniformBuffer worldTransform; + private readonly List intersections = new List(128); // pooled to reduce GC private WorldMesh? worldMesh; private WorldCollider? worldCollider; @@ -150,13 +151,13 @@ public WorldViewer(ITagContainer diContainer) rayRenderer.Material.LinkTransformsTo(world: worldTransform); AddDisposable(rayRenderer); + var globalAssetRegistry = diContainer.GetTag(); localDiContainer = diContainer.ExtendedWith(camera, locationBuffer); AddDisposable(localDiContainer); localDiContainer .AddTag(ecsWorld = new DefaultEcs.World()) - .AddTag(assetRegistry = new AssetLocalRegistry("WorldViewer", localDiContainer)); - AssetRegistry.SubscribeAt(ecsWorld); - assetRegistry.DelayDisposals = false; + .AddTag(assetRegistry = new AssetRegistry(localDiContainer, globalAssetRegistry, "WorldViewer")); + assetRegistry.SubscribeAt(ecsWorld); worldRenderer = new(localDiContainer); AddDisposable(worldRenderer); } @@ -541,29 +542,29 @@ private void HandleRaycast() { shouldUpdate |= SliderFloat("Size", ref intersectionSize, 0.01f, 20f, null, ImGuiSliderFlags.AlwaysClamp); } - shouldUpdate |= Checkbox("Update location", ref updateIntersectionPrimitive); - if (shouldUpdate) - { - if (intersectionPrimitive == IntersectionPrimitive.Ray) - ShootRay(); - else - CheckIntersections(); + shouldUpdate |= Checkbox("Update location", ref updateIntersectionPrimitive); + if (shouldUpdate) + { + if (intersectionPrimitive == IntersectionPrimitive.Ray) + ShootRay(); + else + CheckIntersections(); } - } - + } + private void UpdateIntersectionPrimitive() { if (!updateIntersectionPrimitive) - return; - if (intersectionPrimitive == IntersectionPrimitive.Ray) - ShootRay(); - else - CheckIntersections(); + return; + if (intersectionPrimitive == IntersectionPrimitive.Ray) + ShootRay(); + else + CheckIntersections(); } private void ShootRay() { - if (worldCollider is null) + if (worldCollider is null) return; if (setRaycastToCamera) { @@ -589,15 +590,15 @@ private void ShootRay() rayRenderer.Add(new IColor(255, 0, 255, 255), cast.Value.Point, cast.Value.Point + cast.Value.Normal * 0.2f); } fbArea.IsDirty = true; - } - - private void CheckIntersections() - { - if (worldCollider is null) + } + + private void CheckIntersections() + { + if (worldCollider is null) return; Vector3 center = camera.Location.GlobalPosition; IEnumerable edges; - rayRenderer.Clear(); + rayRenderer.Clear(); intersections.Clear(); switch (intersectionPrimitive) { diff --git a/zzre/tools/effecteditor/EffectEditor.cs b/zzre/tools/effecteditor/EffectEditor.cs index e8760173..a0b6ccf6 100644 --- a/zzre/tools/effecteditor/EffectEditor.cs +++ b/zzre/tools/effecteditor/EffectEditor.cs @@ -37,7 +37,7 @@ private enum TransformMode private readonly GameTime gameTime; private readonly EffectCombiner emptyEffect = new(); private readonly DefaultEcs.World ecsWorld = new(); - private readonly AssetLocalRegistry assetRegistry; + private readonly IAssetRegistry assetRegistry; private readonly SequentialSystem updateSystems = new(); private readonly SequentialSystem renderSystems = new(); private bool isPlaying = true; @@ -83,9 +83,11 @@ public EffectEditor(ITagContainer parentDiContainer) diContainer.AddTag(new EffectMesh(diContainer, 1024, 2048)); diContainer.AddTag(new ModelInstanceBuffer(diContainer, 128)); diContainer.AddTag(camera = new Camera(diContainer)); - diContainer.AddTag(assetRegistry = new AssetLocalRegistry("EffectEditor", diContainer)); - AssetRegistry.SubscribeAt(ecsWorld); - assetRegistry.DelayDisposals = false; + diContainer.AddTag(assetRegistry = new AssetRegistry( + diContainer, + diContainer.GetTag(), + "EffectEditor")); + game.EntityAssetExtensions.SubscribeAt(assetRegistry, ecsWorld); controls = new OrbitControlsTag(Window, camera.Location, diContainer); AddDisposable(controls); @@ -240,7 +242,7 @@ private void HandleGizmos() private void HandleRender(CommandList cl) { - assetRegistry.ApplyAssets(); + assetRegistry.Update(); if (isPlaying && effectEntity.IsAlive) updateSystems.Update(gameTime.Delta); UpdateSoundListener(); diff --git a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs index 792d83a0..f4f69269 100644 --- a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs +++ b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs @@ -24,7 +24,7 @@ private sealed class FOModel : BaseDisposable, ISelectable private readonly ClumpMesh mesh; private readonly AssetHandle meshHandle; private readonly ModelMaterial[] materials; - private readonly List materialHandles = []; + private readonly List materialHandles = []; public Location Location { get; } = new Location(); public zzio.scn.FOModel SceneFOModel { get; } @@ -49,24 +49,26 @@ public FOModel(ITagContainer diContainer, zzio.scn.FOModel sceneModel) Location.LocalRotation = sceneModel.rot.ToZZRotation(); var assetRegistry = diContainer.GetTag(); - meshHandle = assetRegistry.Load(ClumpAsset.Info.Model(sceneModel.filename), AssetLoadPriority.Synchronous).As(); + meshHandle = assetRegistry.LoadModelClump(sceneModel.filename, AssetPriority.Synchronous); mesh = meshHandle.Get().Mesh; if (mesh.IsEmpty) { materials = []; return; } - materials = mesh.Materials.Select(rwMaterial => + materials = [.. mesh.Materials.Select(rwMaterial => { - var material = new ModelMaterial(diContainer); var rwTexture = (RWTexture)rwMaterial.FindChildById(SectionId.Texture, true)!; - var rwTextureName = (RWString)rwTexture.FindChildById(SectionId.String, true)!; - var textureHandle = assetRegistry.LoadTexture(textureBasePaths, rwTextureName.value, AssetLoadPriority.Synchronous, material); + var textureHandle = assetRegistry.TryLoadTextureForMaterial( + textureBasePaths, + rwTexture, + material, + StandardTextureKind.Error); + if (textureHandle.HasValue) + materialHandles.Add(textureHandle.Value); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); - materialHandles.Add(textureHandle); materialHandles.Add(samplerHandle); - material.Texture.Texture = textureHandle.Get().Texture; material.Sampler.Sampler = samplerHandle.Get().Sampler; material.LinkTransformsTo(camera); material.World.BufferRange = locationRange; @@ -74,7 +76,7 @@ public FOModel(ITagContainer diContainer, zzio.scn.FOModel sceneModel) material.Factors.Ref.vertexColorFactor = 0.0f; material.Tint.Ref = rwMaterial.color.ToFColor() * sceneModel.color; return material; - }).ToArray(); + })]; } protected override void DisposeManaged() diff --git a/zzre/tools/sceneeditor/SceneEditor.Model.cs b/zzre/tools/sceneeditor/SceneEditor.Model.cs index 76a04421..480ce5fe 100644 --- a/zzre/tools/sceneeditor/SceneEditor.Model.cs +++ b/zzre/tools/sceneeditor/SceneEditor.Model.cs @@ -25,7 +25,7 @@ private sealed class Model : BaseDisposable, ISelectable private readonly ClumpMesh mesh; private readonly AssetHandle meshHandle; private readonly ModelMaterial[] materials; - private readonly List materialHandles = []; + private readonly List materialHandles = []; public Location Location { get; } = new Location(); public zzio.scn.Model SceneModel { get; } @@ -52,7 +52,7 @@ public Model(ITagContainer diContainer, zzio.scn.Model sceneModel, Behavior? sce Location.LocalRotation = sceneModel.rot.ToZZRotation(); var assetRegistry = diContainer.GetTag(); - meshHandle = assetRegistry.Load(ClumpAsset.Info.Model(sceneModel.filename), AssetLoadPriority.Synchronous).As(); + meshHandle = assetRegistry.LoadModelClump(sceneModel.filename, AssetPriority.Synchronous); mesh = meshHandle.Get().Mesh; if (mesh.IsEmpty) { @@ -64,12 +64,15 @@ public Model(ITagContainer diContainer, zzio.scn.Model sceneModel, Behavior? sce var material = new ModelMaterial(diContainer); var rwTexture = (RWTexture)rwMaterial.FindChildById(SectionId.Texture, true)!; - var rwTextureName = (RWString)rwTexture.FindChildById(SectionId.String, true)!; - var textureHandle = assetRegistry.LoadTexture(textureBasePaths, rwTextureName.value, AssetLoadPriority.Synchronous, material); + var textureHandle = assetRegistry.TryLoadTextureForMaterial( + textureBasePaths, + rwTexture, + material, + StandardTextureKind.Error); + if (textureHandle.HasValue) + materialHandles.Add(textureHandle.Value); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); - materialHandles.Add(textureHandle); materialHandles.Add(samplerHandle); - material.Texture.Texture = textureHandle.Get().Texture; material.Sampler.Sampler = samplerHandle.Get().Sampler; material.LinkTransformsTo(camera); material.World.BufferRange = locationRange; diff --git a/zzre/tools/sceneeditor/SceneEditor.cs b/zzre/tools/sceneeditor/SceneEditor.cs index 738f75fc..44de5045 100644 --- a/zzre/tools/sceneeditor/SceneEditor.cs +++ b/zzre/tools/sceneeditor/SceneEditor.cs @@ -20,7 +20,7 @@ public partial class SceneEditor : ListDisposable, IDocumentEditor private readonly LocationBuffer locationBuffer; private readonly DebugLineRenderer gridRenderer; private readonly Camera camera; - private readonly AssetLocalRegistry assetRegistry; + private readonly IAssetRegistry assetRegistry; private readonly DefaultEcs.World ecsWorld; private event Action OnLoadScene = () => { }; @@ -67,16 +67,16 @@ public SceneEditor(ITagContainer diContainer) fbArea.OnRender += locationBuffer.Update; fbArea.OnRender += gridRenderer.Render; + var globalAssetRegistry = diContainer.GetTag(); localDiContainer = diContainer .FallbackTo(Window) .ExtendedWith(this, Window, gridRenderer, locationBuffer); AddDisposable(localDiContainer); localDiContainer - .AddTag(assetRegistry = new AssetLocalRegistry("SceneEditor", localDiContainer)) + .AddTag(assetRegistry = new AssetRegistry(localDiContainer, globalAssetRegistry, "SceneEditor")) .AddTag(ecsWorld = new DefaultEcs.World()) .AddTag(camera); - AssetRegistry.SubscribeAt(localDiContainer.GetTag()); - assetRegistry.DelayDisposals = false; + game.EntityAssetExtensions.SubscribeAt(assetRegistry, ecsWorld); new MiscComponent(localDiContainer); new DatasetComponent(localDiContainer); new WorldComponent(localDiContainer); @@ -123,7 +123,7 @@ private void LoadSceneNow(IResource resource) Window.Title = $"Scene Editor - {resource.Path.ToPOSIXString()}"; ecsWorld.Publish(new game.messages.SceneLoaded(scene, Savegame: null!)); OnLoadScene(); - assetRegistry.ApplyAssets(); + assetRegistry.Update(); } private void HandleResize() => camera.Aspect = fbArea.Ratio; diff --git a/zzre/tools/validation/Diagnostics.cs b/zzre/tools/validation/Diagnostics.cs new file mode 100644 index 00000000..d251ab67 --- /dev/null +++ b/zzre/tools/validation/Diagnostics.cs @@ -0,0 +1,18 @@ +using System; + +namespace zzre; + +public static partial class Diagnostics +{ + public static readonly DiagnosticCategory CategoryValidation = new("VAL"); + + public static readonly DiagnosticType TypeValIgnoredDueToExtension = + CategoryValidation.Information("Ignored file due to extension: {0}"); + public static Diagnostic ValIgnoredDueToExtension(string file, string ext) => + TypeValIgnoredDueToExtension.Create([ext], [new(file)]); + + public static readonly DiagnosticType TypeValGeneralException = + CategoryValidation.Error("Exception during validation: {0}", footNote: "Stack trace: \n {1}"); + public static Diagnostic ValGeneralException(string file, Exception e) => + TypeValGeneralException.Create([e.Message, e.StackTrace ?? ""], [new(file)]); +} diff --git a/zzre/tools/validation/Validator.cs b/zzre/tools/validation/Validator.cs new file mode 100644 index 00000000..0c58e9a9 --- /dev/null +++ b/zzre/tools/validation/Validator.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Serilog; +using zzio.scn; +using zzio.vfs; +using static zzre.Diagnostics; + +namespace zzre.validation; + +public class Validator(ITagContainer diContainer) +{ + private readonly ILogger logger = diContainer.GetLoggerFor(); + private readonly IResourcePool resourcePool = diContainer.GetTag(); + private readonly IAssetRegistry assetRegistry = diContainer.GetTag(); + private readonly Stopwatch stopwatch = new(); + private readonly List diagnostics = []; + private uint queuedFileCount; + private uint processedFileCount; + private uint faultyFileCount; + + public uint QueuedFileCount => queuedFileCount; + public uint ProcessedFileCount => processedFileCount; + public uint FaultyFileCount => faultyFileCount; + public IReadOnlyList Diagnostics => diagnostics; + + public ushort MaxConcurrency { get; init; } = checked((ushort)Environment.ProcessorCount); + + public Task Run() => Run(CancellationToken.None); + public async Task Run(CancellationToken ct) + { + queuedFileCount = processedFileCount = faultyFileCount = 0; + + stopwatch.Restart(); + logger.Information("Started."); + var resourceTraversalBlock = new TransformManyBlock(TraverseResourcePool, new() + { + MaxDegreeOfParallelism = MaxConcurrency, + CancellationToken = ct + }); + var processResourceBlock = new ActionBlock(ValidateResource, new() + { + BoundedCapacity = MaxConcurrency, + MaxDegreeOfParallelism = MaxConcurrency, + SingleProducerConstrained = true, + CancellationToken = ct + }); + resourceTraversalBlock.LinkTo(processResourceBlock, new() + { + PropagateCompletion = true + }); + + await resourceTraversalBlock.SendAsync(resourcePool); + resourceTraversalBlock.Complete(); + await processResourceBlock.Completion; + stopwatch.Stop(); + + diagnostics.Sort(); + } + + public void LogSummary() + { + int countInfos = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Info); + int countWarns = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Warning); + int countErrors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error); + int countIntErrors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.InternalError); + logger.Information("Validation of {ProcessedFileCount} resources finished in {Elapsed}", processedFileCount, stopwatch.Elapsed); + if (countInfos > 0) + logger.Information($"Informations: {countInfos}"); + if (countWarns > 0) + logger.Information($" Warnings: {countWarns}"); + if (countErrors > 0) + logger.Information($" Errors: {countErrors}"); + if (countIntErrors > 0) + logger.Information($" Int. Errors: {countIntErrors}"); + } + + private IEnumerable TraverseResourcePool(IResourcePool pool) + { + if (pool.Root.Type is ResourceType.File) + { + Interlocked.Increment(ref queuedFileCount); + yield return pool.Root; + yield break; + } + var queue = new Queue(); + queue.Enqueue(pool.Root); + while (queue.TryDequeue(out var resource)) + { + foreach (var file in resource.Files) + { + Interlocked.Increment(ref queuedFileCount); + yield return file; + } + queue.EnsureCapacity(queue.Count + resource.Directories.Count()); + foreach (var dir in resource.Directories) + queue.Enqueue(dir); + } + } + + private async Task ValidateResource(IResource resource) + { + bool wasIgnored = false; + try + { + var ext = Path.GetExtension(resource.Name).ToLowerInvariant(); + switch (ext) + { + case ".bsp": await ValidateWorld(resource); break; + case ".scn": ValidateScene(resource); break; + case ".bmp": + case ".dds": await ValidateTexture(resource); break; + case ".aed": await ValidateActor(resource); break; + case ".ed": await ValidateEffect(resource); break; + case ".dff": await ValidateClump(resource); break; + default: + AddDiagnostic(ValIgnoredDueToExtension(resource.Path.ToString(), ext)); + wasIgnored = true; + break; + } + } + catch (Exception e) + { + AddDiagnostic(ValGeneralException(resource.Path.ToString(), e)); + Interlocked.Increment(ref faultyFileCount); + } + finally + { + if (!wasIgnored) + Interlocked.Increment(ref processedFileCount); + } + } + + private async Task ValidateWorld(IResource resource) + { + using var handle = assetRegistry.LoadWorld(resource.Path, AssetPriority.High); + var world = await handle.GetAsync(CancellationToken.None); + var collider = WorldCollider.Create(world.Mesh.World); + } + + private async Task ValidateClump(IResource resource) + { + using var handle = assetRegistry.Load(new(resource.Path), AssetPriority.High); + var world = await handle.GetAsync(CancellationToken.None); + var collider = GeometryCollider.Create(world.Mesh.Geometry, location: null); + } + + private async Task ValidateTexture(IResource resource) + { + using var handle = assetRegistry.LoadTexture(resource.Path, AssetPriority.High); + await handle.GetAsync(CancellationToken.None); + } + + private static void ValidateScene(IResource resource) + { + using var stream = resource.OpenContent() ?? + throw new IOException($"Could not open scene {resource.Name}"); + var scene = new Scene(); + scene.Read(stream); + } + + private async Task ValidateActor(IResource resource) + { + using var handle = assetRegistry.LoadActor( + Path.GetFileNameWithoutExtension(resource.Name), + AssetPriority.High); + await handle.GetAsync(CancellationToken.None); + } + + private async Task ValidateEffect(IResource resource) + { + using var handle = assetRegistry.LoadEffectCombiner(resource.Path, AssetPriority.High); + await handle.GetAsync(CancellationToken.None); + } + + private void AddDiagnostic(Diagnostic diagnostic) + { + lock (diagnostics) + diagnostics.Add(diagnostic); + } +}