From 63c7f4d610376a10ce74b4f985ef34a71f9cbe9c Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 3 Jun 2025 10:12:14 +0200 Subject: [PATCH 01/64] Remove old asset registry --- .gitignore | 1 + zzio.sln | 15 -- zzre.core/assetregistry/Asset.cs | 246 ------------------ zzre.core/assetregistry/AssetHandle.cs | 162 ------------ zzre.core/assetregistry/AssetHandleScope.cs | 78 ------ zzre.core/assetregistry/AssetInfoRegistry.cs | 108 -------- zzre.core/assetregistry/AssetLocalRegistry.cs | 79 ------ .../assetregistry/AssetRegistry,Debug.cs | 51 ---- zzre.core/assetregistry/AssetRegistry.ECS.cs | 23 -- .../assetregistry/AssetRegistry.Internal.cs | 176 ------------- zzre.core/assetregistry/AssetRegistry.cs | 241 ----------------- zzre.core/assetregistry/AssetRegistryStats.cs | 67 ----- zzre.core/assetregistry/IAssetRegistry.cs | 96 ------- 13 files changed, 1 insertion(+), 1342 deletions(-) delete mode 100644 zzre.core/assetregistry/Asset.cs delete mode 100644 zzre.core/assetregistry/AssetHandle.cs delete mode 100644 zzre.core/assetregistry/AssetHandleScope.cs delete mode 100644 zzre.core/assetregistry/AssetInfoRegistry.cs delete mode 100644 zzre.core/assetregistry/AssetLocalRegistry.cs delete mode 100644 zzre.core/assetregistry/AssetRegistry,Debug.cs delete mode 100644 zzre.core/assetregistry/AssetRegistry.ECS.cs delete mode 100644 zzre.core/assetregistry/AssetRegistry.Internal.cs delete mode 100644 zzre.core/assetregistry/AssetRegistry.cs delete mode 100644 zzre.core/assetregistry/AssetRegistryStats.cs delete mode 100644 zzre.core/assetregistry/IAssetRegistry.cs 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/zzio.sln b/zzio.sln index 64f4ecc4..d8d7462e 100644 --- a/zzio.sln +++ b/zzio.sln @@ -13,8 +13,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzio_dbsqlite", "zzio_dbsql EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzsc", "zzsc\zzsc.csproj", "{DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre", "zzre\zzre.csproj", "{B1A31FB7-7537-4407-8810-5FFB712FD87B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre.core", "zzre.core\zzre.core.csproj", "{CA61C447-0011-4FE7-A358-48EFF2709E68}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre.core.tests", "zzre.core.tests\zzre.core.tests.csproj", "{489E0003-E309-477B-B112-4ADDCDA99B2A}" @@ -104,18 +102,6 @@ Global {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x64.Build.0 = Release|Any CPU {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x86.ActiveCfg = Release|Any CPU {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x86.Build.0 = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x64.ActiveCfg = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x64.Build.0 = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x86.ActiveCfg = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x86.Build.0 = Debug|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|Any CPU.Build.0 = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x64.ActiveCfg = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x64.Build.0 = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x86.ActiveCfg = Release|Any CPU - {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x86.Build.0 = Release|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -161,7 +147,6 @@ Global {29A2E64F-8041-4D0E-B1BF-5E1A9BA17351} = {734E1888-6627-4A7F-8921-F7462F9CF686} {EB62CEEA-164C-4BC8-8CF9-87828B5E4C58} = {78DA8A29-9D93-4F5F-9420-47959E92360A} {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B} = {78DA8A29-9D93-4F5F-9420-47959E92360A} - {B1A31FB7-7537-4407-8810-5FFB712FD87B} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} {CA61C447-0011-4FE7-A358-48EFF2709E68} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} {489E0003-E309-477B-B112-4ADDCDA99B2A} = {734E1888-6627-4A7F-8921-F7462F9CF686} {BE8DA2D7-E8FC-4767-894F-CD760F90D5CF} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} 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 deleted file mode 100644 index 09c6712e..00000000 --- a/zzre.core/assetregistry/AssetHandle.cs +++ /dev/null @@ -1,162 +0,0 @@ -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 -{ - /// 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; - - /// 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) - { - this.handleScope = handleScope; - this.registryInternal = registry as IAssetRegistryInternal ?? - throw new ArgumentException("Cannot create asset handles from registry decorators", nameof(registry)); - AssetID = assetId; - } - - internal AssetHandle(AssetHandleScope handleScope, Guid assetId) - { - this.handleScope = handleScope; - registryInternal = (handleScope as IAssetRegistry).InternalRegistry; - AssetID = assetId; - } - - internal AssetHandle(AssetRegistry registry, Guid assetId) - { - registryInternal = registry; - AssetID = assetId; - } - - /// Disposes the stake on the asset this handle is tied to - /// *May* trigger disposal of the asset and related secondary assets - public void Dispose() - { - if (wasDisposed) - return; - wasDisposed = true; - if (handleScope is null) - registryInternal?.DisposeHandle(this); - else - handleScope.DisposeHandle(this); - } - - [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 - { - CheckDisposed(); - return registryInternal.GetLoadedAsset(AssetID); - } - - /// 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 - { - CheckDisposed(); - return (AssetHandle)this; - } - - /// 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) - { - CheckDisposed(); - registryInternal.AddApplyAction(this, applyFnptr, in applyContext); - } - - /// 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) - { - CheckDisposed(); - registryInternal.AddApplyAction(this, applyAction, in applyContext); - } - - /// 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) - { - CheckDisposed(); - registryInternal.AddApplyAction(this, applyAction); - } - - 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); -} 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/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 deleted file mode 100644 index 3748991b..00000000 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Serilog; - -namespace zzre; - -/// A global asset registry to facilitate loading, retrieval and disposal of assets -public sealed partial class AssetRegistry : zzio.BaseDisposable, IAssetRegistryInternal -{ - private static readonly int MaxLowPriorityAssetsPerFrame = Math.Max(1, Environment.ProcessorCount); - private static readonly UnboundedChannelOptions ChannelOptions = new() - { - AllowSynchronousContinuations = true, - SingleReader = true, - 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 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; - - /// 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) { } - - internal AssetRegistry(string debugName, ITagContainer diContainer, IAssetRegistry? apparentRegistry) - { - DIContainer = diContainer; - this.apparentRegistry = apparentRegistry ?? this; - if (string.IsNullOrEmpty(debugName)) - logger = diContainer.GetLoggerFor(); - else - logger = diContainer.GetTag().For($"{nameof(AssetRegistry)}-{debugName}"); - } - - protected override void DisposeManaged() - { - EnsureMainThread(); - 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(); - assets.Clear(); - assetsToRemove.Writer.Complete(); - assetsToApply.Writer.Complete(); - assetsToStart.Writer.Complete(); - cancellationSource.Dispose(); - logger.Verbose("Finished disposing registry"); - } - - private IAsset GetOrCreateAsset(in TInfo info) - where TInfo : IEquatable - { - 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"); - - var guid = AssetInfoRegistry.ToGuid(info); - lock(assets) - { - if (!assets.TryGetValue(guid, out var asset) || asset.State is AssetState.Disposed) - { - 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; - } - asset.ThrowIfError(); - return asset; - } - } - - /// - public unsafe AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - delegate* managed applyFnptr, - in TApplyContext applyContext) - where TInfo : IEquatable - { - var asset = GetOrCreateAsset(in info); - lock (asset) - { - if (asset is { State: AssetState.Loaded } && IsMainThread) - { - // 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)); - } - } - - private static unsafe Action ConvertFnptr( - delegate* managed fnptr, - in TContext context) - { - var contextCopy = context; - return handle => fnptr(handle, in contextCopy); - } - - /// - public AssetHandle Load( - in TInfo info, - AssetLoadPriority priority, - Action? applyAction) - where TInfo : IEquatable - { - var asset = GetOrCreateAsset(in info); - lock (asset) - { - if (asset is { State: AssetState.Loaded } && IsMainThread) - { - asset.AddRef(); - var handle = new AssetHandle(this, asset.ID); - applyAction?.Invoke(handle); - return handle; - } - return LoadInner(asset, priority, applyAction); - } - } - - private AssetHandle LoadInner(IAsset asset, AssetLoadPriority priority, Action? applyAction) - { - // We assume that asset is locked for our thread during this method - asset.AddRef(); - var handle = new AssetHandle(this, asset.ID); - switch(asset.State) - { - case AssetState.Disposed or AssetState.Error: - throw new ArgumentException("LoadInner was called with asset in unexpected state"); - - 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; - - case AssetState.Loaded: - if (IsMainThread) - applyAction?.Invoke(handle); - else if (applyAction is not null) - assetsToApply.Writer.WriteAsync(asset, Cancellation).AsTask().WaitAndRethrow(); - return handle; - - default: throw new NotImplementedException($"Unimplemented asset state {asset.State}"); - } - } - - private void StartLoading(IAsset asset, AssetLoadPriority priority) - { - // 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}"); - } - } - - private void RemoveAsset(IAsset asset) - { - 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); - } - - private void ApplyAsset(IAsset asset) - { - if (!asset.LoadTask.IsCompleted) - throw new InvalidOperationException("Cannot apply assets that are not (internally) loaded"); - asset.ApplyAction.Invoke(new(this, asset.ID)); - } - - /// - public void ApplyAssets() - { - EnsureMainThread(); - while (assetsToRemove.Reader.TryRead(out var asset)) - RemoveAsset(asset); - while (assetsToApply.Reader.TryRead(out var asset)) - ApplyAsset(asset); - - for (int i = 0; i < MaxLowPriorityAssetsPerFrame && assetsToStart.Reader.TryRead(out var asset); i++) - { - lock (asset) - { - if (asset.State == AssetState.Queued) - asset.StartLoading(); - } - } - } - - [Conditional("DEBUG")] - private void EnsureMainThread([CallerMemberName] string methodName = "") - { - if (!IsMainThread) - throw new InvalidOperationException($"Cannot call AssetRegistry.{methodName} from secondary threads"); - } -} diff --git a/zzre.core/assetregistry/AssetRegistryStats.cs b/zzre.core/assetregistry/AssetRegistryStats.cs deleted file mode 100644 index 5cfdcf04..00000000 --- a/zzre.core/assetregistry/AssetRegistryStats.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Text; -using System.Threading; - -namespace zzre; - -/// -/// A snapshot of mostly monotonous counters of an -/// -public struct AssetRegistryStats -{ - private int created; - private int loaded; - private int removed; - private int total; - - /// The number of assets created - public int Created => created; - /// The number of assets that finished loading - public int Loaded => loaded; - /// The number of assets removed from the registry - public int Removed => removed; - /// The number of currently registered assets - /// This counter is not monotonous - public int Total => total; - - internal void OnAssetCreated() - { - Interlocked.Increment(ref created); - Interlocked.Increment(ref total); - } - - internal void OnAssetLoaded() => Interlocked.Increment(ref loaded); - - internal void OnAssetRemoved() - { - Interlocked.Increment(ref removed); - Interlocked.Decrement(ref total); - } - - public static AssetRegistryStats operator -(AssetRegistryStats lhs, AssetRegistryStats rhs) => new() - { - created = lhs.created - rhs.created, - loaded = lhs.loaded - rhs.loaded, - removed = lhs.removed - rhs.removed, - total = lhs.total - rhs.total, - }; - - public static AssetRegistryStats operator +(AssetRegistryStats lhs, AssetRegistryStats rhs) => new() - { - created = rhs.created + lhs.created, - loaded = rhs.loaded + lhs.loaded, - removed = rhs.removed + lhs.removed, - total = rhs.total + lhs.total, - }; - - public override string ToString() - { - var builder = new StringBuilder(256); - builder.Append("Created: "); - builder.Append(created); - builder.Append(" Loaded: "); - builder.Append(loaded); - builder.Append(" Removed: "); - builder.Append(removed); - return builder.ToString(); - } -} diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs deleted file mode 100644 index 21bb0d4f..00000000 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Threading; -namespace zzre; - -/// Controls when an asset is actually loaded -public enum AssetLoadPriority -{ - /// 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 - ITagContainer DIContainer { get; } - /// Basic statistics of the registry, containing mostly monotonous counters - /// This property will include the statistics of parent registries - 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); -} - -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); -} From 93873681c2c393d4aab5079ccc6b0790d8c910ef Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 5 Jun 2025 17:12:58 +0200 Subject: [PATCH 02/64] Scribble most of AssetRegistry --- zzre.core/NullDisposable.cs | 10 + zzre.core/assetregistry/AssetHandle.cs | 193 ++++++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 238 ++++++++++++++++++++++ zzre.core/assetregistry/IAssetLoader.cs | 38 ++++ zzre.core/assetregistry/IAssetRegistry.cs | 41 ++++ zzre.core/zzre.core.csproj | 1 + 6 files changed, 521 insertions(+) create mode 100644 zzre.core/NullDisposable.cs create mode 100644 zzre.core/assetregistry/AssetHandle.cs create mode 100644 zzre.core/assetregistry/AssetRegistry.cs create mode 100644 zzre.core/assetregistry/IAssetLoader.cs create mode 100644 zzre.core/assetregistry/IAssetRegistry.cs 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/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs new file mode 100644 index 00000000..ad15c76c --- /dev/null +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using DotNext; + +namespace zzre; + +/*public record struct AssetHandle(IAssetRegistry Registry, Guid AssetId) : IDisposable +{ + private bool wasDisposed; + private IDisposable? _asset; + private IDisposable? AssetOpt + { + get => Volatile.Read(ref _asset); + set => Interlocked.Exchange(ref _asset, value); + } + + internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed, IDisposable? asset) : this(registry, assetId) + { + this.wasDisposed = true; + this.AssetOpt = asset; + } + + public void Dispose() + { + if (wasDisposed) return; + wasDisposed = true; + AssetOpt = null; + ((IAssetRegistryInternal)Registry).DelRef(AssetId); + } + + public IDisposable? Asset + { + get + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + if (AssetOpt is null) + { + var result = ((IAssetRegistryInternal)Registry).GetAsset(AssetId).Value; + if (result?.IsSuccessful is true) + AssetOpt = result.Value.Value; + } + return AssetOpt; + } + } + + public IDisposable Get() + { + Registry.ThrowIfNotMainThread(); + if (AssetOpt is not null) + return AssetOpt; + var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); + if (!lazy.IsValueCreated) + lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation); + return AssetOpt = lazy.Value!.Value.Value; // throws on error + } + + public ValueTask GetAsync(CancellationToken ct) + { + if (AssetOpt is IDisposable prevAsset) + return ValueTask.FromResult(prevAsset); + return new(DoGetAsync(ct)); + } + + private async Task DoGetAsync(CancellationToken ct) + { + var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); + return AssetOpt = await lazy.WithCancellation(ct); + } + + public AssetHandle As() where TAsset : class, IDisposable + { + var result = new AssetHandle(Registry, AssetId, wasDisposed, (TAsset?)AssetOpt); + wasDisposed = true; + AssetOpt = null; + return result; + } + + public AssetHandle AsDuplicate() where TAsset : class, IDisposable + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false, AssetOpt); + } + + public AssetHandle Duplicate() + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false, AssetOpt); + } +}*/ + + +public interface IAssetHandle : IDisposable +{ + IAssetRegistry Registry { get; } + Guid AssetId { get; } +} + +public record struct AssetHandle(IAssetRegistry Registry, Guid AssetId) : IAssetHandle + where TAsset : class, IDisposable +{ + private bool wasDisposed; + + internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed) : this(registry, assetId) + { + this.wasDisposed = wasDisposed; + } + + public void Dispose() + { + if (wasDisposed) return; + wasDisposed = true; + ((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) + lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation); + 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() + { + var result = new AssetHandle(Registry, AssetId, wasDisposed, AssetOpt); + wasDisposed = true; + AssetOpt = null; + return result; + } + + public AssetHandle AsDuplicate() + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false, AssetOpt); + }*/ + + public AssetHandle Move() + { + var result = this; + wasDisposed = true; + return result; + } + + public readonly AssetHandle Duplicate() + { + ThrowIfDisposed(); + ((IAssetRegistryInternal)Registry).AddRef(AssetId); + return new(Registry, AssetId, false); + } + + private readonly void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(Registry.WasDisposed, typeof(IAssetRegistry)); + } +} diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs new file mode 100644 index 00000000..201cb76d --- /dev/null +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DotNext; +using DotNext.Threading; +using Serilog; +using Serilog.Core; + +namespace zzre; + +internal class AssetState +{ + public required AsyncLazy LoadLazy; + public IAssetHandle[] Secondaries = []; + public IDisposable? Asset; + public int RefCount = 1; + + internal IAssetHandle[] Dispose() + { + RefCount = 0; + Asset?.Dispose(); + var secondaries = Secondaries; + Secondaries = []; + LoadLazy = AssetRegistry.NullAssetLoadLazy; + return secondaries; + } +} + +public class AssetRegistry : IAssetRegistryInternal +{ + private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(3); + internal static readonly AsyncLazy NullAssetLoadLazy = new(NullDisposable.Instance); + + private readonly Dictionary loaders = []; + private readonly Dictionary assets = []; + private readonly CancellationTokenSource cancellationSource = new(); + private readonly SemaphoreSlim semaphore = new(1, 1); + private readonly ILogger logger; + private readonly int mainThreadId; + private readonly AssetRegistry? parentRegistry; + + public bool WasDisposed => cancellationSource.IsCancellationRequested; + public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; + public bool IsLocalRegistry => ParentRegistry is not null; + public IAssetRegistry? ParentRegistry => ParentRegistry; + public CancellationToken Cancellation => cancellationSource.Token; + + public AssetRegistry(AssetRegistry? parent = null, ILogger? logger = null) + { + if (parent is { IsLocalRegistry: true }) + throw new ArgumentException("Cannot use a local registry as parent"); + parentRegistry = parent; + this.logger = logger ?? Logger.None; + mainThreadId = Environment.CurrentManagedThreadId; + } + + public void Dispose() + { + if (WasDisposed) + return; + if (semaphore.Wait(LockTimeout, Cancellation)) + logger.Warning("AssetRegistry could not lock during dispose, going ahead nonetheless"); + cancellationSource.Cancel(); + loaders.Clear(); + foreach (var asset in assets.Values) + { + var secondaries = asset.Dispose(); + foreach (var secondary in secondaries) + { + if (secondary.Registry != this) + secondary.Dispose(); + } + } + assets.Clear(); + semaphore.Dispose(); + cancellationSource.Dispose(); + } + + public void StartNextLowBatch() + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + throw new NotImplementedException(); + } + + void IAssetRegistryInternal.AddRef(Guid assetId) + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (!semaphore.Wait(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock registry"); + try + { + var assetState = assets.GetValueOrDefault(assetId); + ObjectDisposedException.ThrowIf(assetState is null || assetState.RefCount <= 0, typeof(AssetState)); + assetState.RefCount++; + } + finally + { + semaphore.Release(); + } + } + + void IAssetRegistryInternal.DelRef(Guid assetId) + { + if (WasDisposed) return; // Ignore out-of-order deletion, all assets are already dead + if (!semaphore.Wait(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock registry"); + try + { + 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) + { + assetState.Dispose(); + assets.Remove(assetId); + } + } + finally + { + semaphore.Release(); + } + } + + AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) => + assets.GetValueOrDefault(assetId)?.LoadLazy ?? NullAssetLoadLazy; + + public Task WaitForAll(IEnumerable assetHandles, CancellationToken ct) + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (assetHandles.Any(h => h.Registry != this && h.Registry != ParentRegistry)) + throw new ArgumentException("Cannot wait for assets from a foreign registry"); + return Task.WhenAll(assetHandles + .Select(id => + assets.GetValueOrDefault(id.AssetId) ?? + parentRegistry?.assets.GetValueOrDefault(id.AssetId)) + .Where(state => + state != null && + state.LoadLazy != NullAssetLoadLazy) + .Select(state => state!.LoadLazy.WithCancellation(ct))); + } + + public AssetHandle Load(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if ((parentRegistry is not null && !parentRegistry.loaders.TryGetValue(typeof(TInfo), out var loader)) || + !loaders.TryGetValue(typeof(TInfo), out loader)) + throw new ArgumentException($"No loader registered for info type: {typeof(TInfo).FullName}"); + if (loader.AssetType != typeof(TAsset)) + throw new ArgumentException($"Registered loader is for asset type {loader.AssetType.FullName} and not for {typeof(TAsset).FullName}"); + if (loader.Locality is not AssetLocality.Global && !IsLocalRegistry) + throw new ArgumentException($"Cannot load a local asset {typeof(TAsset).FullName} from a global registry"); + if (loader.Locality is AssetLocality.Global && IsLocalRegistry) + return parentRegistry!.Load(info, priority); + var typedLoader = loader as IAssetLoader; + Debug.Assert(typedLoader is not null); + + var (assetId, assetState) = GetOrCreateAssetState(typedLoader, info); + var handle = new AssetHandle(this, assetId); + if (!assetState.LoadLazy.IsValueCreated) + { + switch (priority) + { + case AssetPriority.Synchronous: + try + { + handle.Get(); // checks main thread + } + catch (Exception) + { + // 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(() => handle.GetAsync(Cancellation), Cancellation); + break; + case AssetPriority.Low: + throw new NotImplementedException(); + } + } + return handle; + } + + private (Guid, AssetState) GetOrCreateAssetState(IAssetLoader typedLoader, in TInfo info) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable + { + var assetId = typedLoader!.InfoToAssetId(info); + + if (!semaphore.Wait(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock registry, what is happening?"); + try + { + if (assets.TryGetValue(assetId, out var assetState) && assetState.RefCount > 0) + { + assetState.RefCount++; + return (assetId, assetState); + } + + TInfo infoCopy = info; + assetState = new() + { + LoadLazy = new(ct => LoadAsset(typedLoader, infoCopy, assetId)) + }; + assets[assetId] = assetState; + return (assetId, assetState); + } + finally + { + semaphore.Release(); + } + } + + public void RegisterLoader(IAssetLoader loader) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (ParentRegistry is not null) + { + ParentRegistry.RegisterLoader(loader); + return; + } + } + + private Task LoadAsset(IAssetLoader loader, in TInfo info, Guid assetId) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable + { + return Task.FromResult(NullDisposable.Instance); + } +} diff --git a/zzre.core/assetregistry/IAssetLoader.cs b/zzre.core/assetregistry/IAssetLoader.cs new file mode 100644 index 00000000..eba13170 --- /dev/null +++ b/zzre.core/assetregistry/IAssetLoader.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace zzre; + +public enum AssetLocality +{ + Global, + Local, + Unique +} + +public interface IAssetLoader +{ + AssetLocality Locality { get; } + Type InfoType { get; } + Type AssetType { get; } + IAssetRegistry Registry { get; } +} + +public readonly record struct AssetLoadResult( + TAsset Asset, + IReadOnlyList SecondaryAssets +) where TAsset : class, IDisposable; + +public interface IAssetLoader : IAssetLoader + where TInfo : struct, IEquatable + where TAsset : class, IDisposable +{ + Type IAssetLoader.InfoType => typeof(TInfo); + Type IAssetLoader.AssetType => typeof(TAsset); + + Guid InfoToAssetId(in TInfo info); + Task> Load(Guid AssetId, in TInfo info, CancellationToken ct); +} + diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs new file mode 100644 index 00000000..c2c33e8c --- /dev/null +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DotNext.Threading; + +namespace zzre; + +public enum AssetPriority +{ + Synchronous, + High, + Low +} + +public interface IAssetRegistry : IDisposable +{ + bool WasDisposed { get; } + bool IsMainThread { get; } + IAssetRegistry? ParentRegistry { get; } + bool IsLocalRegistry => ParentRegistry is not null; + CancellationToken Cancellation { get; } // is triggered when registry is disposed + + void RegisterLoader(IAssetLoader loader) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable; + + AssetHandle Load(in TInfo info, AssetPriority priority) + where TInfo : struct, IEquatable + where TAsset : class, IDisposable; + + void StartNextLowBatch(); + Task WaitForAll(IEnumerable assets, CancellationToken ct); +} + +internal interface IAssetRegistryInternal : IAssetRegistry +{ + void AddRef(Guid assetId); + void DelRef(Guid assetId); + AsyncLazy GetAsset(Guid assetId); +} 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 @@ + From 8d03352d9939e2a826f961a8fdc553fd83787230 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 5 Jun 2025 17:39:32 +0200 Subject: [PATCH 03/64] IAsset instead of IAssetLoader --- zzre.core/assetregistry/AssetRegistry.cs | 86 ++++++++++++++--------- zzre.core/assetregistry/IAsset.cs | 35 +++++++++ zzre.core/assetregistry/IAssetLoader.cs | 38 ---------- zzre.core/assetregistry/IAssetRegistry.cs | 6 +- 4 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 zzre.core/assetregistry/IAsset.cs delete mode 100644 zzre.core/assetregistry/IAssetLoader.cs diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 201cb76d..d1de09df 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DotNext; using DotNext.Threading; using Serilog; using Serilog.Core; @@ -15,7 +13,15 @@ internal class AssetState { public required AsyncLazy LoadLazy; public IAssetHandle[] Secondaries = []; - public IDisposable? Asset; + public IDisposable? Asset + { + get + { + var result = LoadLazy.Value; + return result is null || !result.Value.IsSuccessful + ? null : result.Value.Value; + } + } public int RefCount = 1; internal IAssetHandle[] Dispose() @@ -34,7 +40,6 @@ public class AssetRegistry : IAssetRegistryInternal private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(3); internal static readonly AsyncLazy NullAssetLoadLazy = new(NullDisposable.Instance); - private readonly Dictionary loaders = []; private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); private readonly SemaphoreSlim semaphore = new(1, 1); @@ -64,7 +69,6 @@ public void Dispose() if (semaphore.Wait(LockTimeout, Cancellation)) logger.Warning("AssetRegistry could not lock during dispose, going ahead nonetheless"); cancellationSource.Cancel(); - loaders.Clear(); foreach (var asset in assets.Values) { var secondaries = asset.Dispose(); @@ -144,22 +148,15 @@ public Task WaitForAll(IEnumerable assetHandles, CancellationToken public AssetHandle Load(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable - where TAsset : class, IDisposable + where TAsset : class, IAsset { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - if ((parentRegistry is not null && !parentRegistry.loaders.TryGetValue(typeof(TInfo), out var loader)) || - !loaders.TryGetValue(typeof(TInfo), out loader)) - throw new ArgumentException($"No loader registered for info type: {typeof(TInfo).FullName}"); - if (loader.AssetType != typeof(TAsset)) - throw new ArgumentException($"Registered loader is for asset type {loader.AssetType.FullName} and not for {typeof(TAsset).FullName}"); - if (loader.Locality is not AssetLocality.Global && !IsLocalRegistry) + if (TAsset.Locality is not AssetLocality.Global && !IsLocalRegistry) throw new ArgumentException($"Cannot load a local asset {typeof(TAsset).FullName} from a global registry"); - if (loader.Locality is AssetLocality.Global && IsLocalRegistry) + if (TAsset.Locality is AssetLocality.Global && IsLocalRegistry) return parentRegistry!.Load(info, priority); - var typedLoader = loader as IAssetLoader; - Debug.Assert(typedLoader is not null); - var (assetId, assetState) = GetOrCreateAssetState(typedLoader, info); + var (assetId, assetState) = GetOrCreateAssetState(info); var handle = new AssetHandle(this, assetId); if (!assetState.LoadLazy.IsValueCreated) { @@ -187,11 +184,11 @@ public AssetHandle Load(in TInfo info, AssetPriority prio return handle; } - private (Guid, AssetState) GetOrCreateAssetState(IAssetLoader typedLoader, in TInfo info) + private (Guid, AssetState) GetOrCreateAssetState(in TInfo info) where TInfo : struct, IEquatable - where TAsset : class, IDisposable + where TAsset : class, IAsset { - var assetId = typedLoader!.InfoToAssetId(info); + var assetId = TAsset.InfoToAssetId(info); if (!semaphore.Wait(LockTimeout, Cancellation)) throw new InvalidOperationException("Could not lock registry, what is happening?"); @@ -206,7 +203,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio TInfo infoCopy = info; assetState = new() { - LoadLazy = new(ct => LoadAsset(typedLoader, infoCopy, assetId)) + LoadLazy = new(ct => LoadAsset(infoCopy, assetId)) }; assets[assetId] = assetState; return (assetId, assetState); @@ -217,22 +214,47 @@ public AssetHandle Load(in TInfo info, AssetPriority prio } } - public void RegisterLoader(IAssetLoader loader) + private async Task LoadAsset(TInfo info, Guid assetId) where TInfo : struct, IEquatable - where TAsset : class, IDisposable + where TAsset : class, IAsset { - ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - if (ParentRegistry is not null) + // Due to AsyncLazy we can flow exceptions outside this method + + // Load asset and secondary assets + var (asset, secondaries) = await TAsset.LoadAsync(info, Cancellation); + if (secondaries.Any()) { - ParentRegistry.RegisterLoader(loader); - return; + try + { + await WaitForAll(secondaries, Cancellation); + } + finally + { + asset.Dispose(); + } } - } - private Task LoadAsset(IAssetLoader loader, in TInfo info, Guid assetId) - where TInfo : struct, IEquatable - where TAsset : class, IDisposable - { - return Task.FromResult(NullDisposable.Instance); + // Propagate assets into registry state + if (!await semaphore.WaitAsync(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock registry, what is happening?"); + try + { + var assetState = assets.GetValueOrDefault(assetId); + ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); + assetState.Secondaries = [.. secondaries]; + } + catch (Exception) + { + asset.Dispose(); + foreach (var secondary in secondaries) + secondary.Dispose(); + throw; + } + finally + { + semaphore.Release(); + } + + return asset; } } diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs new file mode 100644 index 00000000..28d2fd4b --- /dev/null +++ b/zzre.core/assetregistry/IAsset.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace zzre; + +public enum AssetLocality +{ + Global, + Local, + Unique +} + +public interface IAsset : IDisposable +{ + static abstract AssetLocality Locality { get; } + static abstract Type InfoType { get; } + IAssetRegistry Registry { get; } +} + +public readonly record struct AssetLoadResult( + IAsset Asset, + IReadOnlyList SecondaryAssets +) where TInfo : struct, IEquatable; + +public interface IAsset : IAsset + where TInfo : struct, IEquatable +{ + static Type IAsset.InfoType => typeof(TInfo); + + static abstract Guid InfoToAssetId(in TInfo info); + static abstract Task> LoadAsync(in TInfo info, CancellationToken ct); +} + diff --git a/zzre.core/assetregistry/IAssetLoader.cs b/zzre.core/assetregistry/IAssetLoader.cs deleted file mode 100644 index eba13170..00000000 --- a/zzre.core/assetregistry/IAssetLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace zzre; - -public enum AssetLocality -{ - Global, - Local, - Unique -} - -public interface IAssetLoader -{ - AssetLocality Locality { get; } - Type InfoType { get; } - Type AssetType { get; } - IAssetRegistry Registry { get; } -} - -public readonly record struct AssetLoadResult( - TAsset Asset, - IReadOnlyList SecondaryAssets -) where TAsset : class, IDisposable; - -public interface IAssetLoader : IAssetLoader - where TInfo : struct, IEquatable - where TAsset : class, IDisposable -{ - Type IAssetLoader.InfoType => typeof(TInfo); - Type IAssetLoader.AssetType => typeof(TAsset); - - Guid InfoToAssetId(in TInfo info); - Task> Load(Guid AssetId, in TInfo info, CancellationToken ct); -} - diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index c2c33e8c..2261ff2b 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -21,13 +21,9 @@ public interface IAssetRegistry : IDisposable bool IsLocalRegistry => ParentRegistry is not null; CancellationToken Cancellation { get; } // is triggered when registry is disposed - void RegisterLoader(IAssetLoader loader) - where TInfo : struct, IEquatable - where TAsset : class, IDisposable; - AssetHandle Load(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable - where TAsset : class, IDisposable; + where TAsset : class, IAsset; void StartNextLowBatch(); Task WaitForAll(IEnumerable assets, CancellationToken ct); From 0c3af474dc98db82bc1e01cd7471b5784098e8b2 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 11 Jun 2025 08:49:42 +0200 Subject: [PATCH 04/64] General guids, low batches --- zzre.core/assetregistry/AssetRegistry.cs | 75 ++++++++++++++++++++---- zzre.core/assetregistry/IAsset.cs | 15 ++++- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index d1de09df..1a910250 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using DotNext.Threading; using Serilog; @@ -9,7 +11,7 @@ namespace zzre; -internal class AssetState +internal sealed class AssetState { public required AsyncLazy LoadLazy; public IAssetHandle[] Secondaries = []; @@ -39,7 +41,15 @@ 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, + SingleReader = true, + SingleWriter = false + }; + private readonly Channel assetsToStart = Channel.CreateUnbounded(ChannelOptions); private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); private readonly SemaphoreSlim semaphore = new(1, 1); @@ -83,12 +93,6 @@ public void Dispose() cancellationSource.Dispose(); } - public void StartNextLowBatch() - { - ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - throw new NotImplementedException(); - } - void IAssetRegistryInternal.AddRef(Guid assetId) { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); @@ -178,8 +182,10 @@ public AssetHandle Load(in TInfo info, AssetPriority prio Task.Run(() => handle.GetAsync(Cancellation), Cancellation); break; case AssetPriority.Low: - throw new NotImplementedException(); - } + var success = assetsToStart.Writer.TryWrite(assetId); + Debug.Assert(success); // As the channel is unbounded it should never fail to write + break; + } } return handle; } @@ -188,18 +194,31 @@ public AssetHandle Load(in TInfo info, AssetPriority prio where TInfo : struct, IEquatable where TAsset : class, IAsset { - var assetId = TAsset.InfoToAssetId(info); - if (!semaphore.Wait(LockTimeout, Cancellation)) throw new InvalidOperationException("Could not lock registry, what is happening?"); try { + // Determine Asset ID + Guid assetId; + if (TAsset.Locality is AssetLocality.Unique) + { + do + { + 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++; return (assetId, assetState); } + // Create new asset state TInfo infoCopy = info; assetState = new() { @@ -214,6 +233,14 @@ public AssetHandle Load(in TInfo info, AssetPriority prio } } + [Conditional("DEBUG")] + private static void SanityCheckSharedAsset(Type expectedType, AssetState asset) + { + if (asset.Asset is null) return; + var actualType = asset.Asset.GetType(); + Debug.Assert(actualType.IsAssignableTo(expectedType), "Asset type mismatch, is this a GUID conflict?"); + } + private async Task LoadAsset(TInfo info, Guid assetId) where TInfo : struct, IEquatable where TAsset : class, IAsset @@ -257,4 +284,30 @@ private async Task LoadAsset(TInfo info, Guid assetI return asset; } + + public void StartNextLowBatch() => + StartNextLowBatch(MaxLowPriorityAssetsPerFrame); + + public void StartNextLowBatch(int maxAssets) + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); + if (!IsMainThread) + throw new InvalidOperationException("Low batch scheduling is only allowed on the main thread"); + if (!semaphore.Wait(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock registry, what is happening?"); + try + { + for (int i = 0; i < maxAssets && 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); + } + } + finally + { + semaphore.Release(); + } + } } diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 28d2fd4b..6285bf58 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -17,6 +17,19 @@ public interface IAsset : IDisposable static abstract AssetLocality Locality { get; } static abstract Type InfoType { get; } IAssetRegistry Registry { get; } + + private static readonly object generalInfoLock = new(); + private static readonly Dictionary generalInfoToGuid = []; + internal static Guid GeneralInfoToGuid(object info) + { + lock (generalInfoLock) + { + if (generalInfoToGuid.TryGetValue(info, out var guid)) + return guid; + generalInfoToGuid.Add(info, guid = Guid.NewGuid()); + return guid; + } + } } public readonly record struct AssetLoadResult( @@ -29,7 +42,7 @@ public interface IAsset : IAsset { static Type IAsset.InfoType => typeof(TInfo); - static abstract Guid InfoToAssetId(in TInfo info); + static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); static abstract Task> LoadAsync(in TInfo info, CancellationToken ct); } From c1d5ba9b0e57e45042b7a9c1ff0f54c13165f47d Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 11 Jun 2025 08:56:36 +0200 Subject: [PATCH 05/64] DI for asset loading --- zzre.core/assetregistry/AssetRegistry.cs | 12 +++++++++--- zzre.core/assetregistry/IAsset.cs | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 1a910250..15d49b14 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -62,13 +62,18 @@ public class AssetRegistry : IAssetRegistryInternal public bool IsLocalRegistry => ParentRegistry is not null; public IAssetRegistry? ParentRegistry => ParentRegistry; public CancellationToken Cancellation => cancellationSource.Token; + public ITagContainer DIContainer { get; } - public AssetRegistry(AssetRegistry? parent = null, ILogger? logger = null) + public AssetRegistry(ITagContainer diContainer, AssetRegistry? parent = null, string? debugName = null) { + DIContainer = diContainer; if (parent is { IsLocalRegistry: true }) throw new ArgumentException("Cannot use a local registry as parent"); parentRegistry = parent; - this.logger = logger ?? Logger.None; + 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; } @@ -91,6 +96,7 @@ public void Dispose() assets.Clear(); semaphore.Dispose(); cancellationSource.Dispose(); + logger.Verbose("Finished disposing registry"); } void IAssetRegistryInternal.AddRef(Guid assetId) @@ -248,7 +254,7 @@ private async Task LoadAsset(TInfo info, Guid assetI // Due to AsyncLazy we can flow exceptions outside this method // Load asset and secondary assets - var (asset, secondaries) = await TAsset.LoadAsync(info, Cancellation); + var (asset, secondaries) = await TAsset.LoadAsync(this, info, Cancellation); if (secondaries.Any()) { try diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 6285bf58..9c5673ac 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -43,6 +43,6 @@ public interface IAsset : IAsset static Type IAsset.InfoType => typeof(TInfo); static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); - static abstract Task> LoadAsync(in TInfo info, CancellationToken ct); + static abstract Task> LoadAsync(IAssetRegistry registry, in TInfo info, CancellationToken ct); } From fde4034f18c1267a24b5f0098b926e9828c4abb5 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 12 Jun 2025 10:13:04 +0200 Subject: [PATCH 06/64] Some trivial registry tests --- zzre.core.tests/TestAssetRegistry.cs | 118 +++++++++++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 5 +- zzre.core/assetregistry/IAsset.cs | 8 +- 3 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 zzre.core.tests/TestAssetRegistry.cs diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs new file mode 100644 index 00000000..83f21927 --- /dev/null +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -0,0 +1,118 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace zzre.tests; + +[TestFixture] +public class TestAssetRegistry +{ + private interface ITestAsset : IAsset + { + public IAssetRegistry Registry { get; init; } + public int Id { get; init; } + } + + private readonly record struct TestInfo(int Id, Func? CreateSecondaries = null) : IEquatable + { + public readonly TaskCompletionSource StartedLoad = new(); + public readonly TaskCompletionSource FinishLoad = new(); + + public TestInfo(int Id, IAssetHandle[] secondaries) : this(Id, () => secondaries) { } + + public readonly TestInfo AsCompleted() + { + FinishLoad.SetResult(); + 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; + ct.ThrowIfCancellationRequested(); + return new( + new TAsset() { Id = info.Id, Registry = registry }, + info.CreateSecondaries?.Invoke()); + } + } + + private class GlobalTestAsset : ITestAsset + { + public static AssetLocality Locality => AssetLocality.Global; + public IAssetRegistry Registry { get; init; } + public int Id { get; init; } + + public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + => TestInfo.LoadAsync(registry, info, ct); + + public void Dispose() => Volatile.Write(ref WasDisposed, true); + public bool WasDisposed; + } + + private class LocalTestAsset : ITestAsset + { + public static AssetLocality Locality => AssetLocality.Local; + public IAssetRegistry Registry { get; init; } + public int Id { get; init; } + + public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + => TestInfo.LoadAsync(registry, info, ct); + + public void Dispose() => Volatile.Write(ref WasDisposed, true); + public bool WasDisposed; + } + + private class UniqueTestAsset : ITestAsset + { + public static AssetLocality Locality => AssetLocality.Unique; + public IAssetRegistry Registry { get; init; } + public int Id { get; init; } + + public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + => TestInfo.LoadAsync(registry, info, ct); + + public void Dispose() => Volatile.Write(ref WasDisposed, true); + public bool WasDisposed; + } + + private readonly TagContainer DI = new(); + + [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); + } + + [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(); + } +} diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 15d49b14..7bdc20fd 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -60,13 +60,14 @@ public class AssetRegistry : IAssetRegistryInternal public bool WasDisposed => cancellationSource.IsCancellationRequested; public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; public bool IsLocalRegistry => ParentRegistry is not null; - public IAssetRegistry? ParentRegistry => ParentRegistry; + public IAssetRegistry? ParentRegistry => parentRegistry; public CancellationToken Cancellation => cancellationSource.Token; public ITagContainer DIContainer { get; } public AssetRegistry(ITagContainer diContainer, AssetRegistry? parent = null, string? debugName = null) { DIContainer = diContainer; + 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; @@ -255,6 +256,8 @@ private async Task LoadAsset(TInfo info, Guid assetI // Load asset and secondary assets var (asset, secondaries) = await TAsset.LoadAsync(this, info, Cancellation); + Debug.Assert(asset.Registry == this); + secondaries ??= []; if (secondaries.Any()) { try diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 9c5673ac..b099c9b4 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -14,8 +14,8 @@ public enum AssetLocality public interface IAsset : IDisposable { - static abstract AssetLocality Locality { get; } - static abstract Type InfoType { get; } + public static abstract AssetLocality Locality { get; } + public static abstract Type InfoType { get; } IAssetRegistry Registry { get; } private static readonly object generalInfoLock = new(); @@ -34,7 +34,7 @@ internal static Guid GeneralInfoToGuid(object info) public readonly record struct AssetLoadResult( IAsset Asset, - IReadOnlyList SecondaryAssets + IReadOnlyList? SecondaryAssets = null ) where TInfo : struct, IEquatable; public interface IAsset : IAsset @@ -43,6 +43,6 @@ public interface IAsset : IAsset static Type IAsset.InfoType => typeof(TInfo); static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); - static abstract Task> LoadAsync(IAssetRegistry registry, in TInfo info, CancellationToken ct); + static abstract Task> LoadAsync(IAssetRegistry registry, TInfo info, CancellationToken ct); } From 75fb2a6570ef8e4e7c722d4cb280c31bda7fde12 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 13 Jun 2025 09:00:52 +0200 Subject: [PATCH 07/64] MainThreadDisposal and various tests --- zzre.core.tests/TestAssetRegistry.cs | 166 ++++++++++++++++++++-- zzre.core.tests/zzre.core.tests.csproj | 2 +- zzre.core/assetregistry/AssetRegistry.cs | 55 ++++--- zzre.core/assetregistry/IAsset.cs | 27 ++-- zzre.core/assetregistry/IAssetRegistry.cs | 2 +- 5 files changed, 210 insertions(+), 42 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 83f21927..e8d60eeb 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1,23 +1,28 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; namespace zzre.tests; -[TestFixture] +[TestFixture, CancelAfter(3000), SingleThreaded] public class TestAssetRegistry { private interface ITestAsset : IAsset { public IAssetRegistry Registry { get; init; } - public int Id { get; init; } + public TestInfo Info { get; init; } + public int Id => Info.Id; } - private readonly record struct TestInfo(int Id, Func? CreateSecondaries = null) : IEquatable + private readonly struct TestInfo(int Id, Func? CreateSecondaries = null) : IEquatable { + public readonly int Id = Id; + public readonly Func? CreateSecondaries = null; public readonly TaskCompletionSource StartedLoad = new(); public readonly TaskCompletionSource FinishLoad = new(); + public readonly TaskCompletionSource Disposed = new(); public TestInfo(int Id, IAssetHandle[] secondaries) : this(Id, () => secondaries) { } @@ -35,21 +40,47 @@ public static async Task> LoadAsync(IAssetRegi await info.FinishLoad.Task; ct.ThrowIfCancellationRequested(); return new( - new TAsset() { Id = info.Id, Registry = registry }, + new TAsset() { Info = info, Registry = registry }, info.CreateSecondaries?.Invoke()); } + + 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(); } private class GlobalTestAsset : ITestAsset { public static AssetLocality Locality => AssetLocality.Global; public IAssetRegistry Registry { get; init; } - public int Id { get; init; } + public TestInfo Info { get; init; } public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); - public void Dispose() => Volatile.Write(ref WasDisposed, true); + 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, TestInfo info, CancellationToken ct) + => TestInfo.LoadAsync(registry, info, ct); + + public void Dispose() + { + Volatile.Write(ref WasDisposed, true); + Info.Disposed.TrySetResult(); + } public bool WasDisposed; } @@ -57,12 +88,16 @@ private class LocalTestAsset : ITestAsset { public static AssetLocality Locality => AssetLocality.Local; public IAssetRegistry Registry { get; init; } - public int Id { get; init; } + public TestInfo Info { get; init; } public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); - public void Dispose() => Volatile.Write(ref WasDisposed, true); + public void Dispose() + { + Volatile.Write(ref WasDisposed, true); + Info.Disposed.TrySetResult(); + } public bool WasDisposed; } @@ -70,12 +105,16 @@ private class UniqueTestAsset : ITestAsset { public static AssetLocality Locality => AssetLocality.Unique; public IAssetRegistry Registry { get; init; } - public int Id { get; init; } + public TestInfo Info { get; init; } public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); - public void Dispose() => Volatile.Write(ref WasDisposed, true); + public void Dispose() + { + Volatile.Write(ref WasDisposed, true); + Info.Disposed.TrySetResult(); + } public bool WasDisposed; } @@ -115,4 +154,111 @@ public void RegistryDisposeOrder() global.Dispose(); local.Dispose(); } + + private void CommonAssetChecks(IAssetRegistry registry, AssetHandle handle, int id) + where TAsset : class, ITestAsset => Assert.Multiple(() => + { + 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 LoadSyncGlobal_Single() + { + using var global = new AssetRegistry(DI); + using var handle = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + CommonAssetChecks(global, handle, 1); + } + + [Test] + public void LoadSyncGlobal_MultipleDiff() + { + using var global = new AssetRegistry(DI); + using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + using var handle2 = global.Load(new TestInfo(42).AsCompleted(), AssetPriority.Synchronous); + using var handle3 = global.Load(new TestInfo(1337).AsCompleted(), AssetPriority.Synchronous); + CommonAssetChecks(global, handle1, 1); + CommonAssetChecks(global, handle2, 42); + CommonAssetChecks(global, handle3, 1337); + } + + [Test] + public void LoadSyncGlobal_MultipleSame() + { + using var global = new AssetRegistry(DI); + using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + using var handle2 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + using var handle3 = global.Load(new TestInfo(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 void DisposeAsset_MultiSyncRefs() + { + using var global = new AssetRegistry(DI); + var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var handle2 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var handle3 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + 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(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + handle = thandle; + asset = thandle.Asset!; + } + else + { + var thandle = global.Load(new TestInfo(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); + } + } } 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/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 7bdc20fd..684d35bc 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -13,6 +13,7 @@ namespace zzre; internal sealed class AssetState { + public required bool NeedsMainThreadDisposal; public required AsyncLazy LoadLazy; public IAssetHandle[] Secondaries = []; public IDisposable? Asset @@ -25,16 +26,6 @@ public IDisposable? Asset } } public int RefCount = 1; - - internal IAssetHandle[] Dispose() - { - RefCount = 0; - Asset?.Dispose(); - var secondaries = Secondaries; - Secondaries = []; - LoadLazy = AssetRegistry.NullAssetLoadLazy; - return secondaries; - } } public class AssetRegistry : IAssetRegistryInternal @@ -50,6 +41,7 @@ public class AssetRegistry : IAssetRegistryInternal }; 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 SemaphoreSlim semaphore = new(1, 1); @@ -87,7 +79,7 @@ public void Dispose() cancellationSource.Cancel(); foreach (var asset in assets.Values) { - var secondaries = asset.Dispose(); + var secondaries = DisposeAssetState(asset); foreach (var secondary in secondaries) { if (secondary.Registry != this) @@ -95,11 +87,30 @@ public void Dispose() } } assets.Clear(); + DisposeOldAssets(); // after current assets in case we add something into it (we shouldn't) + semaphore.Dispose(); cancellationSource.Dispose(); logger.Verbose("Finished disposing registry"); } + private IAssetHandle[] DisposeAssetState(AssetState state) + { + if (IsMainThread || !state.NeedsMainThreadDisposal) + state.Asset?.Dispose(); + else if (state.Asset is not null) + { + var success = assetsToDispose.Writer.TryWrite(state.Asset); + Debug.Assert(success); + } + + state.RefCount = 0; + var secondaries = state.Secondaries; + state.Secondaries = []; + state.LoadLazy = NullAssetLoadLazy; + return secondaries; + } + void IAssetRegistryInternal.AddRef(Guid assetId) { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); @@ -129,7 +140,7 @@ void IAssetRegistryInternal.DelRef(Guid assetId) return; // Let's just ignore already-dead assets, we got what we wanted if (--assetState.RefCount <= 0) { - assetState.Dispose(); + DisposeAssetState(assetState); assets.Remove(assetId); } } @@ -229,6 +240,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio TInfo infoCopy = info; assetState = new() { + NeedsMainThreadDisposal = TAsset.NeedsMainThreadDisposal, LoadLazy = new(ct => LoadAsset(infoCopy, assetId)) }; assets[assetId] = assetState; @@ -293,11 +305,11 @@ private async Task LoadAsset(TInfo info, Guid assetI return asset; } - - public void StartNextLowBatch() => - StartNextLowBatch(MaxLowPriorityAssetsPerFrame); - public void StartNextLowBatch(int maxAssets) + public void Update() => + Update(MaxLowPriorityAssetsPerFrame); + + public void Update(int maxLowPrioAssets) { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); if (!IsMainThread) @@ -306,7 +318,8 @@ public void StartNextLowBatch(int maxAssets) throw new InvalidOperationException("Could not lock registry, what is happening?"); try { - for (int i = 0; i < maxAssets && assetsToStart.Reader.TryRead(out var assetId); i++) + DisposeOldAssets(); + for (int i = 0; i < maxLowPrioAssets && assetsToStart.Reader.TryRead(out var assetId); i++) { if (assets.TryGetValue(assetId, out var assetState) && assetState.LoadLazy != NullAssetLoadLazy && @@ -319,4 +332,12 @@ public void StartNextLowBatch(int maxAssets) semaphore.Release(); } } + + private void DisposeOldAssets() + { + Debug.Assert(IsMainThread); + Debug.Assert(semaphore.CurrentCount == 0); + while (assetsToDispose.Reader.TryRead(out var asset)) + asset.Dispose(); + } } diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index b099c9b4..24716e2d 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -16,20 +16,8 @@ public interface IAsset : IDisposable { public static abstract AssetLocality Locality { get; } public static abstract Type InfoType { get; } + public static virtual bool NeedsMainThreadDisposal => false; IAssetRegistry Registry { get; } - - private static readonly object generalInfoLock = new(); - private static readonly Dictionary generalInfoToGuid = []; - internal static Guid GeneralInfoToGuid(object info) - { - lock (generalInfoLock) - { - if (generalInfoToGuid.TryGetValue(info, out var guid)) - return guid; - generalInfoToGuid.Add(info, guid = Guid.NewGuid()); - return guid; - } - } } public readonly record struct AssetLoadResult( @@ -44,5 +32,18 @@ public interface IAsset : IAsset static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); static abstract Task> LoadAsync(IAssetRegistry registry, 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 2261ff2b..4c01c94f 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -25,7 +25,7 @@ AssetHandle Load(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable where TAsset : class, IAsset; - void StartNextLowBatch(); + void Update(); Task WaitForAll(IEnumerable assets, CancellationToken ct); } From c417ec9742899ff0f2bd980b7fb51f6cc5f7da52 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 13 Jun 2025 11:09:16 +0200 Subject: [PATCH 08/64] Many load tests --- zzre.core.tests/TestAssetRegistry.cs | 332 ++++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 5 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index e8d60eeb..d932bc84 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -37,7 +37,7 @@ public static async Task> LoadAsync(IAssetRegi { ct.ThrowIfCancellationRequested(); Assert.That(info.StartedLoad.TrySetResult(), $"Asset {info.Id} was tried to be loaded twice"); - await info.FinishLoad.Task; + await info.FinishLoad.Task.WaitAsync(ct); ct.ThrowIfCancellationRequested(); return new( new TAsset() { Info = info, Registry = registry }, @@ -153,11 +153,20 @@ public void RegistryDisposeOrder() 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(); } - private void CommonAssetChecks(IAssetRegistry registry, AssetHandle handle, int id) + 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)); @@ -165,7 +174,7 @@ private void CommonAssetChecks(IAssetRegistry registry, AssetHandle(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); @@ -173,7 +182,7 @@ public void LoadSyncGlobal_Single() } [Test] - public void LoadSyncGlobal_MultipleDiff() + public void LoadSync_MultipleDiff() { using var global = new AssetRegistry(DI); using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); @@ -185,7 +194,7 @@ public void LoadSyncGlobal_MultipleDiff() } [Test] - public void LoadSyncGlobal_MultipleSame() + public void LoadSync_MultipleSame() { using var global = new AssetRegistry(DI); using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); @@ -201,6 +210,319 @@ public void LoadSyncGlobal_MultipleSame() Assert.That(handle1.Asset, Is.SameAs(handle3.Asset)); } + [Test] + public async Task LoadHigh_Single(CancellationToken ct) + { + using var global = new AssetRegistry(DI); + var info = new TestInfo(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 = new TestInfo(1); + var info2 = new TestInfo(2); + var info3 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 async Task LoadLow_Single(CancellationToken ct) + { + using var global = new AssetRegistry(DI); + var info = new TestInfo(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 = new TestInfo(1); + var info2 = new TestInfo(2); + var info3 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 = new TestInfo(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 LoadSequential([Values] AssetPriority prio1, [Values] AssetPriority prio2, CancellationToken ct) + { + using var global = new AssetRegistry(DI); + var info = new TestInfo(1); + + var (handle1, asset1) = await LoadAsset(prio1); + var (handle2, asset2) = await LoadAsset(prio2); + CommonAssetChecks(global, handle1, 1, asset1); + CommonAssetChecks(global, handle2, 1, asset2); + + async Task<(AssetHandle, GlobalTestAsset)> LoadAsset(AssetPriority prio) + { + 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(new TestInfo(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 void DisposeAsset_AccessAfter() + { + using var global = new AssetRegistry(DI); + var handle = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + handle.Dispose(); + + Assert.That(handle.Get, Throws.InstanceOf()); + } + [Test] public void DisposeAsset_MultiSyncRefs() { From 604a5ff1024bec0a16f4ab6c3b281b1f3334ee86 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 13 Jun 2025 11:42:39 +0200 Subject: [PATCH 09/64] Asset disposal tests --- zzre.core.tests/TestAssetRegistry.cs | 111 ++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 18 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index d932bc84..2e713911 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -474,30 +474,72 @@ public async Task LoadLow_MultipleSame_Interleaved(CancellationToken ct) } [Test] - public async Task LoadSequential([Values] AssetPriority prio1, [Values] AssetPriority prio2, CancellationToken ct) + public async Task LoadLow_DelRefBeforeLoad(CancellationToken ct) { using var global = new AssetRegistry(DI); var info = new TestInfo(1); - var (handle1, asset1) = await LoadAsset(prio1); - var (handle2, asset2) = await LoadAsset(prio2); + 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 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); + } - async Task<(AssetHandle, GlobalTestAsset)> LoadAsset(AssetPriority prio) - { - if (prio is AssetPriority.Synchronous) - info.FinishLoad.TrySetResult(); - var handle = global.Load(info, prio); + [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); - if (prio is AssetPriority.Synchronous) - return (handle, handle.Get()); - if (prio is AssetPriority.Low) - global.Update(); + 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 async Task<(AssetHandle, GlobalTestAsset)> CommonLoadAsset1( + AssetRegistry global, + AssetPriority prio, + CancellationToken ct) + { + var info = new TestInfo(1); + if (prio is AssetPriority.Synchronous) info.FinishLoad.TrySetResult(); - return (handle, await Task.Run(() => handle.GetAsync(ct).AsTask(), ct)); - } + 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] @@ -524,12 +566,16 @@ public void DisposeAsset_AccessAfter() } [Test] - public void DisposeAsset_MultiSyncRefs() + public async Task DisposeAsset_MultiRefs( + [Values] AssetPriority prio1, + [Values] AssetPriority prio2, + [Values] AssetPriority prio3, + CancellationToken ct) { using var global = new AssetRegistry(DI); - var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); - var handle2 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); - var handle3 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + 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); @@ -583,4 +629,33 @@ await Task.Run(() => }, ct); } } + + [Test] + public async Task DisposeAsset_DuringHighLoad(CancellationToken ct) + { + using var global = new AssetRegistry(DI); + var info = new TestInfo(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 = new TestInfo(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); + } } From f22f5791258adbf36eded3f496f0bb8148a06898 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 13 Jun 2025 13:52:12 +0200 Subject: [PATCH 10/64] Misc tests --- zzre.core.tests/TestAssetRegistry.cs | 137 +++++++++++++++++++-------- 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 2e713911..d03eeda2 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -6,7 +6,10 @@ namespace zzre.tests; -[TestFixture, CancelAfter(3000), SingleThreaded] +[TestFixture(TaskContinuationOptions.None)] +[TestFixture(TaskContinuationOptions.RunContinuationsAsynchronously)] +[TestFixture(TaskContinuationOptions.ExecuteSynchronously)] +[CancelAfter(3000), SingleThreaded] public class TestAssetRegistry { private interface ITestAsset : IAsset @@ -16,15 +19,17 @@ private interface ITestAsset : IAsset public int Id => Info.Id; } - private readonly struct TestInfo(int Id, Func? CreateSecondaries = null) : IEquatable + private readonly struct TestInfo(TaskContinuationOptions tcsOptions, + int Id, Func? CreateSecondaries = null) : IEquatable { public readonly int Id = Id; public readonly Func? CreateSecondaries = null; - public readonly TaskCompletionSource StartedLoad = new(); - public readonly TaskCompletionSource FinishLoad = new(); - public readonly TaskCompletionSource Disposed = new(); + public readonly TaskCompletionSource StartedLoad = new(tcsOptions); + public readonly TaskCompletionSource FinishLoad = new(tcsOptions); + public readonly TaskCompletionSource Disposed = new(tcsOptions); - public TestInfo(int Id, IAssetHandle[] secondaries) : this(Id, () => secondaries) { } + public TestInfo(TaskContinuationOptions tcsOptions, int Id, IAssetHandle[] secondaries) + : this(tcsOptions, Id, () => secondaries) { } public readonly TestInfo AsCompleted() { @@ -119,6 +124,12 @@ public void Dispose() } 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() @@ -177,7 +188,7 @@ private void CommonAssetChecks(IAssetRegistry registry, AssetHandle(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + using var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); CommonAssetChecks(global, handle, 1); } @@ -185,9 +196,9 @@ public void LoadSync_Single() public void LoadSync_MultipleDiff() { using var global = new AssetRegistry(DI); - using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); - using var handle2 = global.Load(new TestInfo(42).AsCompleted(), AssetPriority.Synchronous); - using var handle3 = global.Load(new TestInfo(1337).AsCompleted(), AssetPriority.Synchronous); + 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); @@ -197,9 +208,9 @@ public void LoadSync_MultipleDiff() public void LoadSync_MultipleSame() { using var global = new AssetRegistry(DI); - using var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); - using var handle2 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); - using var handle3 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + 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); @@ -214,7 +225,7 @@ public void LoadSync_MultipleSame() public async Task LoadHigh_Single(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); // uncompleted + var info = GetInfo(1); // uncompleted using var handle = global.Load(info, AssetPriority.High); Assert.That(handle.Asset, Is.Null); @@ -229,9 +240,9 @@ public async Task LoadHigh_Single(CancellationToken ct) public async Task LoadHigh_MultipleDiff(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info1 = new TestInfo(1); - var info2 = new TestInfo(2); - var info3 = new TestInfo(3); + 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); @@ -262,7 +273,7 @@ public async Task LoadHigh_MultipleDiff(CancellationToken ct) public async Task LoadHigh_MultipleSame_Parallel1(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + 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); @@ -287,7 +298,7 @@ public async Task LoadHigh_MultipleSame_Parallel1(CancellationToken ct) public async Task LoadHigh_MultipleSame_Parallel2(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + 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); @@ -308,7 +319,7 @@ public async Task LoadHigh_MultipleSame_Parallel2(CancellationToken ct) public async Task LoadHigh_MultipleSame_Sequential(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1).AsCompleted(); + var info = GetInfo(1).AsCompleted(); using var handle1 = global.Load(info, AssetPriority.High); var asset1 = await handle1.GetAsync(ct); @@ -324,7 +335,7 @@ public async Task LoadHigh_MultipleSame_Sequential(CancellationToken ct) public async Task LoadHigh_MultipleSame_Interleaved(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + var info = GetInfo(1); using var handle1 = global.Load(info, AssetPriority.High); await info.StartedLoad.Task.WaitAsync(ct); @@ -338,11 +349,38 @@ public async Task LoadHigh_MultipleSame_Interleaved(CancellationToken ct) 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 = new TestInfo(1); // uncompleted + var info = GetInfo(1); // uncompleted using var handle = global.Load(info, AssetPriority.Low); Assert.That(handle.Asset, Is.Null); @@ -358,9 +396,9 @@ public async Task LoadLow_Single(CancellationToken ct) public async Task LoadLow_MultipleDiff(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info1 = new TestInfo(1); - var info2 = new TestInfo(2); - var info3 = new TestInfo(3); + 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); @@ -392,7 +430,7 @@ public async Task LoadLow_MultipleDiff(CancellationToken ct) public async Task LoadLow_MultipleSame_Parallel1(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + 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); @@ -418,7 +456,7 @@ public async Task LoadLow_MultipleSame_Parallel1(CancellationToken ct) public async Task LoadLow_MultipleSame_Parallel2(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + 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); @@ -440,7 +478,7 @@ public async Task LoadLow_MultipleSame_Parallel2(CancellationToken ct) public async Task LoadLow_MultipleSame_Sequential(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1).AsCompleted(); + var info = GetInfo(1).AsCompleted(); using var handle1 = global.Load(info, AssetPriority.Low); global.Update(); @@ -458,7 +496,7 @@ public async Task LoadLow_MultipleSame_Sequential(CancellationToken ct) public async Task LoadLow_MultipleSame_Interleaved(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + var info = GetInfo(1); using var handle1 = global.Load(info, AssetPriority.Low); global.Update(); @@ -477,7 +515,7 @@ public async Task LoadLow_MultipleSame_Interleaved(CancellationToken ct) public async Task LoadLow_DelRefBeforeLoad(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + var info = GetInfo(1); using var handle1 = global.Load(info, AssetPriority.High); using var handle2 = global.Load(info, AssetPriority.Low); @@ -528,7 +566,7 @@ public async Task LoadSequential_WithDisposal([Values] AssetPriority prio1, [Val AssetPriority prio, CancellationToken ct) { - var info = new TestInfo(1); + var info = GetInfo(1); if (prio is AssetPriority.Synchronous) info.FinishLoad.TrySetResult(); var handle = global.Load(info, prio); @@ -546,7 +584,7 @@ public async Task LoadSequential_WithDisposal([Values] AssetPriority prio1, [Val public void DisposeRegistry_SyncAssset() { var global = new AssetRegistry(DI); - var handle1 = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); var asset = handle1.Get(); global.Dispose(); @@ -555,11 +593,36 @@ public void DisposeRegistry_SyncAssset() 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(); + 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 DisposeAsset_AccessAfter() { using var global = new AssetRegistry(DI); - var handle = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var handle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); handle.Dispose(); Assert.That(handle.Get, Throws.InstanceOf()); @@ -596,13 +659,13 @@ public async Task DisposeAsset_MultiThreaded([Values] bool needsMainThread, [Val ITestAsset asset; if (needsMainThread) { - var thandle = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); handle = thandle; asset = thandle.Asset!; } else { - var thandle = global.Load(new TestInfo(1).AsCompleted(), AssetPriority.Synchronous); + var thandle = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); handle = thandle; asset = thandle.Asset!; } @@ -634,7 +697,7 @@ await Task.Run(() => public async Task DisposeAsset_DuringHighLoad(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + var info = GetInfo(1); using var handle1 = global.Load(info, AssetPriority.High); await info.StartedLoad.Task.WaitAsync(ct); @@ -648,7 +711,7 @@ public async Task DisposeAsset_DuringHighLoad(CancellationToken ct) public async Task DisposeAsset_DuringLowLoad(CancellationToken ct) { using var global = new AssetRegistry(DI); - var info = new TestInfo(1); + var info = GetInfo(1); using var handle1 = global.Load(info, AssetPriority.Low); global.Update(); From fa2e699ad1219fc004160d9e07b92910dfa03b59 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 13 Jun 2025 14:37:38 +0200 Subject: [PATCH 11/64] Broken error tests --- zzre.core.tests/TestAssetRegistry.cs | 126 +++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index d03eeda2..58735757 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1,8 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace zzre.tests; @@ -37,6 +39,12 @@ public readonly TestInfo AsCompleted() 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() { @@ -721,4 +729,122 @@ public async Task DisposeAsset_DuringLowLoad(CancellationToken ct) await info.Disposed.Task.WaitAsync(ct); } + + private sealed class TestException : Exception + { + + } + private static readonly InstanceOfTypeConstraint ThrowsAssetExceptions = + // TODO: Test inner TestExceptions in aggregated asset exceptions + 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).AsCompleted() + : GetInfo(1).AsErroneous(); + 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 () => handle1.GetAsync(ct), ThrowsAssetExceptions); + + Assert.That(() => + { + handle2.Get(); + }, ThrowsAssetExceptions); + + handle1.Dispose(); + + Assert.That(() => + { + handle2.Get(); + }, ThrowsAssetExceptions); + } } From 0dcd3d8c1dd2ff8a4112c9e004f5eae0749dbfed Mon Sep 17 00:00:00 2001 From: Helco Date: Mon, 23 Jun 2025 09:44:29 +0200 Subject: [PATCH 12/64] DisposeRegistry_HighAsset_DuringLoad test --- zzre.core.tests/TestAssetRegistry.cs | 9 ++++++++- zzre.core/assetregistry/AssetRegistry.cs | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 58735757..a1a95546 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -610,7 +610,14 @@ public async Task DisposeRegistry_HighAsset_DuringLoad(CancellationToken ct) await info.StartedLoad.Task.WaitAsync(ct); global.Dispose(); - Assert.That(info.Disposed.Task.IsCompletedSuccessfully, Is.True); + 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()); } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 684d35bc..b8e10195 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -276,11 +276,13 @@ private async Task LoadAsset(TInfo info, Guid assetI { await WaitForAll(secondaries, Cancellation); } - finally + catch { asset.Dispose(); + throw; } } + CheckRegistryDisposal(); // Propagate assets into registry state if (!await semaphore.WaitAsync(LockTimeout, Cancellation)) @@ -291,7 +293,7 @@ private async Task LoadAsset(TInfo info, Guid assetI ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); assetState.Secondaries = [.. secondaries]; } - catch (Exception) + catch { asset.Dispose(); foreach (var secondary in secondaries) @@ -303,7 +305,19 @@ private async Task LoadAsset(TInfo info, Guid assetI semaphore.Release(); } + CheckRegistryDisposal(); return asset; + + void CheckRegistryDisposal() + { + if (WasDisposed) + { + asset.Dispose(); + foreach (var secondary in secondaries) + secondary.Dispose(); + ObjectDisposedException.ThrowIf(true, typeof(AssetRegistry)); + } + } } public void Update() => From e1b82ccfefc4932dc2a9454104dbec3ec94554c3 Mon Sep 17 00:00:00 2001 From: Helco Date: Mon, 23 Jun 2025 09:53:04 +0200 Subject: [PATCH 13/64] AggregateException vs TestException --- zzre.core.tests/TestAssetRegistry.cs | 5 ++--- zzre.core/assetregistry/AssetHandle.cs | 11 ++++++++++- zzre.core/assetregistry/AssetRegistry.cs | 7 +++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index a1a95546..d651971a 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -741,9 +741,8 @@ private sealed class TestException : Exception { } - private static readonly InstanceOfTypeConstraint ThrowsAssetExceptions = - // TODO: Test inner TestExceptions in aggregated asset exceptions - Throws.InstanceOf(); + private static InstanceOfTypeConstraint ThrowsAssetExceptions => + Throws.InstanceOf(); [Test] public void Error_SingleSync() diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index ad15c76c..903600ad 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -134,7 +134,16 @@ public readonly TAsset Get() throw new InvalidOperationException("Synchronous asset loading is only allowed on the main thread"); var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); if (!lazy.IsValueCreated) - lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation); + { + try + { + lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation); + } + catch (AggregateException e) + { + throw e.InnerException ?? e; + } + } return (TAsset)lazy.Value!.Value; // throws on error } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index b8e10195..702f82fb 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -189,11 +189,14 @@ public AssetHandle Load(in TInfo info, AssetPriority prio { handle.Get(); // checks main thread } - catch (Exception) + catch (Exception e) { // the user does not get the handle, so there shouldn't be a refcount on the asset (this as IAssetRegistryInternal).DelRef(assetId); - throw; + if (e is AggregateException { InnerException: Exception inner }) + throw inner; + else + throw; } break; case AssetPriority.High: From 0b19bbd6546ea7140102731405fd7e42beb43261 Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 24 Jun 2025 17:29:33 +0200 Subject: [PATCH 14/64] Fix Error_* tests --- zzre.core.tests/TestAssetRegistry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index d651971a..6cfd06fa 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -818,8 +818,8 @@ public async Task Error_ResetAfterDispose( AssetHandle? Load(bool withException) { var info = withException - ? GetInfo(1).AsCompleted() - : GetInfo(1).AsErroneous(); + ? GetInfo(1).AsErroneous() + : GetInfo(1).AsCompleted(); if (priority is AssetPriority.Synchronous && withException) { Assert.That(() => global.Load(info, priority), @@ -839,7 +839,7 @@ public async Task Error_NoResetWithRefs(CancellationToken ct) var handle1 = global.Load(info, AssetPriority.High); var handle2 = global.Load(info, AssetPriority.High); - await Assert.ThatAsync(async () => handle1.GetAsync(ct), ThrowsAssetExceptions); + await Assert.ThatAsync(async () => await handle1.GetAsync(ct), ThrowsAssetExceptions); Assert.That(() => { From 032ea5244e5deec71025fe1edca6a887757a6bb6 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 09:01:57 +0200 Subject: [PATCH 15/64] Fix AssetHandle being mutable --- zzre.core/assetregistry/AssetHandle.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index 903600ad..afab3ad3 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -98,10 +98,12 @@ public interface IAssetHandle : IDisposable Guid AssetId { get; } } -public record struct AssetHandle(IAssetRegistry Registry, Guid AssetId) : IAssetHandle +public struct AssetHandle(IAssetRegistry registry, Guid assetId) : IAssetHandle, IEquatable> where TAsset : class, IDisposable { private bool wasDisposed; + public readonly IAssetRegistry Registry => registry; + public readonly Guid AssetId => assetId; internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed) : this(registry, assetId) { @@ -131,7 +133,7 @@ public readonly TAsset Get() { ThrowIfDisposed(); if (!Registry.IsMainThread) - throw new InvalidOperationException("Synchronous asset loading is only allowed on the main thread"); + throw new InvalidOperationException("Synchronous asset loading is only allowed on the main thread"); var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); if (!lazy.IsValueCreated) { @@ -199,4 +201,15 @@ private readonly void ThrowIfDisposed() ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); ObjectDisposedException.ThrowIf(Registry.WasDisposed, 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); } From 403dcbe5ae2b594cde12577d20b134b1248e86c9 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 09:33:48 +0200 Subject: [PATCH 16/64] Add local asset tests --- zzre.core.tests/TestAssetRegistry.cs | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 6cfd06fa..81e010db 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -853,4 +853,37 @@ public async Task Error_NoResetWithRefs(CancellationToken ct) handle2.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); + } } From 031eea8853dead35101467831774cad1f593109d Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 15:51:14 +0200 Subject: [PATCH 17/64] Add initial secondary tests before removing --- zzre.core.tests/TestAssetRegistry.cs | 45 +++++++++++++++++++++++--- zzre.core/assetregistry/AssetHandle.cs | 3 +- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 81e010db..19896b2f 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -25,7 +25,7 @@ private readonly struct TestInfo(TaskContinuationOptions tcsOptions, int Id, Func? CreateSecondaries = null) : IEquatable { public readonly int Id = Id; - public readonly Func? CreateSecondaries = null; + public readonly Func? CreateSecondaries = CreateSecondaries; public readonly TaskCompletionSource StartedLoad = new(tcsOptions); public readonly TaskCompletionSource FinishLoad = new(tcsOptions); public readonly TaskCompletionSource Disposed = new(tcsOptions); @@ -137,7 +137,8 @@ public void Dispose() public TestAssetRegistry(TaskContinuationOptions tcsOptions) => this.tcsOptions = tcsOptions; - private TestInfo GetInfo(int id) => new TestInfo(tcsOptions, id); + private TestInfo GetInfo(int id, Func? createSecondaries = null) => + new TestInfo(tcsOptions, id, createSecondaries); [Test] public void EmptyRegistries() @@ -569,12 +570,18 @@ public async Task LoadSequential_WithDisposal([Values] AssetPriority prio1, [Val Assert.That(asset1, Is.Not.SameAs(asset2)); } - private async Task<(AssetHandle, GlobalTestAsset)> CommonLoadAsset1( + 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) { - var info = GetInfo(1); if (prio is AssetPriority.Synchronous) info.FinishLoad.TrySetResult(); var handle = global.Load(info, prio); @@ -886,4 +893,34 @@ public void Local_LoadLocalFromLocal() var asset = handle.Get(); CommonAssetChecks(local, handle, 1, asset); } + + [Test] + public async Task Secondary_LoadHigh([Values] AssetPriority parentPrio, CancellationToken ct) + { + using var global = new AssetRegistry(DI); + + AssetHandle childHandle = default; + var childInfo = GetInfo(2).AsCompleted(); + var parentInfo = GetInfo(1, () => + [childHandle = global.Load(childInfo, AssetPriority.High)] + ).AsCompleted(); + + var (parentHandle, parentAsset) = await CommonLoadAsset(global, parentInfo, parentPrio, ct); + CommonAssetChecks(global, parentHandle, 1, parentAsset); + CommonAssetChecks(global, childHandle, 2); + } + + [Test] + public async Task Secondary_HighLoadLow(CancellationToken ct) + { + using var global = new AssetRegistry(DI); + + AssetHandle childHandle = default; + var childInfo = GetInfo(2).AsCompleted(); + var parentInfo = GetInfo(1, () => + [childHandle = global.Load(childInfo, AssetPriority.Low)] + ).AsCompleted(); + + + } } diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index afab3ad3..d3abbf9c 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -199,7 +199,8 @@ public readonly AssetHandle Duplicate() private readonly void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); - ObjectDisposedException.ThrowIf(Registry.WasDisposed, typeof(IAssetRegistry)); + ObjectDisposedException.ThrowIf(AssetId == Guid.Empty, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(Registry?.WasDisposed is null or true, typeof(IAssetRegistry)); } public readonly override int GetHashCode() => From 866c2e0dceca24c3b669506c1fa9017bef840c98 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 16:02:13 +0200 Subject: [PATCH 18/64] Remove secondary assets --- zzre.core.tests/TestAssetRegistry.cs | 44 +++--------------------- zzre.core/assetregistry/AssetRegistry.cs | 34 +++--------------- zzre.core/assetregistry/IAsset.cs | 3 +- 3 files changed, 10 insertions(+), 71 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 19896b2f..7389029a 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -21,17 +21,15 @@ private interface ITestAsset : IAsset public int Id => Info.Id; } - private readonly struct TestInfo(TaskContinuationOptions tcsOptions, - int Id, Func? CreateSecondaries = null) : IEquatable + private readonly struct TestInfo(TaskContinuationOptions tcsOptions, int Id) : IEquatable { public readonly int Id = Id; - public readonly Func? CreateSecondaries = CreateSecondaries; 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, () => secondaries) { } + : this(tcsOptions, Id) { } public readonly TestInfo AsCompleted() { @@ -52,9 +50,7 @@ public static async Task> LoadAsync(IAssetRegi 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 }, - info.CreateSecondaries?.Invoke()); + return new(new TAsset() { Info = info, Registry = registry }); } public bool Equals(TestInfo other) => Id == other.Id; @@ -137,8 +133,8 @@ public void Dispose() public TestAssetRegistry(TaskContinuationOptions tcsOptions) => this.tcsOptions = tcsOptions; - private TestInfo GetInfo(int id, Func? createSecondaries = null) => - new TestInfo(tcsOptions, id, createSecondaries); + private TestInfo GetInfo(int id) => + new TestInfo(tcsOptions, id); [Test] public void EmptyRegistries() @@ -893,34 +889,4 @@ public void Local_LoadLocalFromLocal() var asset = handle.Get(); CommonAssetChecks(local, handle, 1, asset); } - - [Test] - public async Task Secondary_LoadHigh([Values] AssetPriority parentPrio, CancellationToken ct) - { - using var global = new AssetRegistry(DI); - - AssetHandle childHandle = default; - var childInfo = GetInfo(2).AsCompleted(); - var parentInfo = GetInfo(1, () => - [childHandle = global.Load(childInfo, AssetPriority.High)] - ).AsCompleted(); - - var (parentHandle, parentAsset) = await CommonLoadAsset(global, parentInfo, parentPrio, ct); - CommonAssetChecks(global, parentHandle, 1, parentAsset); - CommonAssetChecks(global, childHandle, 2); - } - - [Test] - public async Task Secondary_HighLoadLow(CancellationToken ct) - { - using var global = new AssetRegistry(DI); - - AssetHandle childHandle = default; - var childInfo = GetInfo(2).AsCompleted(); - var parentInfo = GetInfo(1, () => - [childHandle = global.Load(childInfo, AssetPriority.Low)] - ).AsCompleted(); - - - } } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 702f82fb..cf00a9cd 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -15,7 +15,6 @@ internal sealed class AssetState { public required bool NeedsMainThreadDisposal; public required AsyncLazy LoadLazy; - public IAssetHandle[] Secondaries = []; public IDisposable? Asset { get @@ -79,12 +78,7 @@ public void Dispose() cancellationSource.Cancel(); foreach (var asset in assets.Values) { - var secondaries = DisposeAssetState(asset); - foreach (var secondary in secondaries) - { - if (secondary.Registry != this) - secondary.Dispose(); - } + DisposeAssetState(asset); } assets.Clear(); DisposeOldAssets(); // after current assets in case we add something into it (we shouldn't) @@ -94,7 +88,7 @@ public void Dispose() logger.Verbose("Finished disposing registry"); } - private IAssetHandle[] DisposeAssetState(AssetState state) + private void DisposeAssetState(AssetState state) { if (IsMainThread || !state.NeedsMainThreadDisposal) state.Asset?.Dispose(); @@ -105,10 +99,7 @@ private IAssetHandle[] DisposeAssetState(AssetState state) } state.RefCount = 0; - var secondaries = state.Secondaries; - state.Secondaries = []; state.LoadLazy = NullAssetLoadLazy; - return secondaries; } void IAssetRegistryInternal.AddRef(Guid assetId) @@ -270,21 +261,8 @@ private async Task LoadAsset(TInfo info, Guid assetI // Due to AsyncLazy we can flow exceptions outside this method // Load asset and secondary assets - var (asset, secondaries) = await TAsset.LoadAsync(this, info, Cancellation); + var asset = (await TAsset.LoadAsync(this, info, Cancellation)).Asset; Debug.Assert(asset.Registry == this); - secondaries ??= []; - if (secondaries.Any()) - { - try - { - await WaitForAll(secondaries, Cancellation); - } - catch - { - asset.Dispose(); - throw; - } - } CheckRegistryDisposal(); // Propagate assets into registry state @@ -294,13 +272,10 @@ private async Task LoadAsset(TInfo info, Guid assetI { var assetState = assets.GetValueOrDefault(assetId); ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); - assetState.Secondaries = [.. secondaries]; } catch { asset.Dispose(); - foreach (var secondary in secondaries) - secondary.Dispose(); throw; } finally @@ -316,10 +291,9 @@ void CheckRegistryDisposal() if (WasDisposed) { asset.Dispose(); - foreach (var secondary in secondaries) - secondary.Dispose(); ObjectDisposedException.ThrowIf(true, typeof(AssetRegistry)); } + Cancellation.ThrowIfCancellationRequested(); } } diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 24716e2d..9c9982ef 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -21,8 +21,7 @@ public interface IAsset : IDisposable } public readonly record struct AssetLoadResult( - IAsset Asset, - IReadOnlyList? SecondaryAssets = null + IAsset Asset ) where TInfo : struct, IEquatable; public interface IAsset : IAsset From e0656bbcfaceb70726e4c9fa30d7f61777564d66 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 16:33:47 +0200 Subject: [PATCH 19/64] Add two load nested tests --- zzre.core.tests/TestAssetRegistry.cs | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 7389029a..66c986c6 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -889,4 +889,48 @@ public void Local_LoadLocalFromLocal() var asset = handle.Get(); CommonAssetChecks(local, handle, 1, asset); } + + [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); + 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 + } } From b2ca44d88ede8ddec0520d552eb26c5d51e6bbec Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 16:48:22 +0200 Subject: [PATCH 20/64] Added deep nested load test --- zzre.core.tests/TestAssetRegistry.cs | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 66c986c6..94a260f2 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -933,4 +933,39 @@ public async Task LoadNested_SyncSecondaryHigh(CancellationToken ct) await loadChildTask.WaitAsync(ct); // just to be sure } + + [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... + + } From 328238ade5a814c59a34c08f5b51aed0921ad710 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 20:53:40 +0200 Subject: [PATCH 21/64] Fix LoadNested_SyncSecondaryHigh tests --- zzre.core.tests/TestAssetRegistry.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 94a260f2..40bd8041 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -924,6 +924,7 @@ public async Task LoadNested_SyncSecondaryHigh(CancellationToken ct) { await parentInfo.StartedLoad.Task; childHandle = global.Load(GetInfo(2).AsCompleted(), AssetPriority.High); + await childHandle.GetAsync(ct); parentInfo.FinishLoad.SetResult(); }, ct); @@ -931,7 +932,8 @@ public async Task LoadNested_SyncSecondaryHigh(CancellationToken ct) CommonAssetChecks(global, parentHandle, 1); CommonAssetChecks(global, childHandle, 2); - await loadChildTask.WaitAsync(ct); // just to be sure + await loadChildTask.WaitAsync(ct); // just to be sure it *finished* + // the secondary should have been ready after that synchronous primary load } [Test] From 3ea1811be280fc025efa00bc5368658ea2e18cbf Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 25 Jun 2025 21:57:20 +0200 Subject: [PATCH 22/64] Add simple tests to increase coverage --- zzre.core.tests/TestAssetRegistry.cs | 161 +++++++++++++++++++++- zzre.core/assetregistry/AssetRegistry.cs | 33 +++-- zzre.core/assetregistry/IAsset.cs | 4 +- zzre.core/assetregistry/IAssetRegistry.cs | 2 +- 4 files changed, 185 insertions(+), 15 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 40bd8041..b2ee9cbc 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -146,6 +146,15 @@ public void EmptyRegistries() 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] @@ -178,6 +187,13 @@ public void RegistryDisposeOrder() 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(() => { @@ -537,6 +553,21 @@ public async Task LoadLow_DelRefBeforeLoad(CancellationToken ct) CommonAssetChecks(global, handle2, 1, asset2); } + [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) { @@ -636,6 +667,28 @@ public async Task DisposeRegistry_LowAsset_BeforeLoad() 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() { @@ -857,6 +910,17 @@ public async Task Error_NoResetWithRefs(CancellationToken ct) }, 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() { @@ -969,5 +1033,100 @@ public async Task LoadNested_Deep(CancellationToken ct) // 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)); + } + + } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index cf00a9cd..a96ecad5 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Channels; @@ -102,11 +103,25 @@ private void DisposeAssetState(AssetState state) state.LoadLazy = NullAssetLoadLazy; } + [ExcludeFromCodeCoverage] // we cannot reasonably check for semaphore failure + private void LockSemaphore() + { + if (!semaphore.Wait(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock asset registry"); + // this should only happen in bug scenarios + } + + [ExcludeFromCodeCoverage] + private async Task LockSemaphoreAsync(CancellationToken ct) + { + if (!await semaphore.WaitAsync(LockTimeout, Cancellation)) + throw new InvalidOperationException("Could not lock asset registry"); + } + void IAssetRegistryInternal.AddRef(Guid assetId) { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - if (!semaphore.Wait(LockTimeout, Cancellation)) - throw new InvalidOperationException("Could not lock registry"); + LockSemaphore(); try { var assetState = assets.GetValueOrDefault(assetId); @@ -119,11 +134,11 @@ void IAssetRegistryInternal.AddRef(Guid assetId) } } + [ExcludeFromCodeCoverage] void IAssetRegistryInternal.DelRef(Guid assetId) { if (WasDisposed) return; // Ignore out-of-order deletion, all assets are already dead - if (!semaphore.Wait(LockTimeout, Cancellation)) - throw new InvalidOperationException("Could not lock registry"); + LockSemaphore(); try { var assetState = assets.GetValueOrDefault(assetId); @@ -206,8 +221,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio where TInfo : struct, IEquatable where TAsset : class, IAsset { - if (!semaphore.Wait(LockTimeout, Cancellation)) - throw new InvalidOperationException("Could not lock registry, what is happening?"); + LockSemaphore(); try { // Determine Asset ID @@ -266,8 +280,7 @@ private async Task LoadAsset(TInfo info, Guid assetI CheckRegistryDisposal(); // Propagate assets into registry state - if (!await semaphore.WaitAsync(LockTimeout, Cancellation)) - throw new InvalidOperationException("Could not lock registry, what is happening?"); + await LockSemaphoreAsync(Cancellation); try { var assetState = assets.GetValueOrDefault(assetId); @@ -286,6 +299,7 @@ private async Task LoadAsset(TInfo info, Guid assetI CheckRegistryDisposal(); return asset; + [ExcludeFromCodeCoverage] // we cannot reasonably test that, it would be a race condition void CheckRegistryDisposal() { if (WasDisposed) @@ -305,8 +319,7 @@ public void Update(int maxLowPrioAssets) ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); if (!IsMainThread) throw new InvalidOperationException("Low batch scheduling is only allowed on the main thread"); - if (!semaphore.Wait(LockTimeout, Cancellation)) - throw new InvalidOperationException("Could not lock registry, what is happening?"); + LockSemaphore(); try { DisposeOldAssets(); diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 9c9982ef..5015e009 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -15,7 +16,6 @@ public enum AssetLocality public interface IAsset : IDisposable { public static abstract AssetLocality Locality { get; } - public static abstract Type InfoType { get; } public static virtual bool NeedsMainThreadDisposal => false; IAssetRegistry Registry { get; } } @@ -27,8 +27,6 @@ IAsset Asset public interface IAsset : IAsset where TInfo : struct, IEquatable { - static Type IAsset.InfoType => typeof(TInfo); - static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); static abstract Task> LoadAsync(IAssetRegistry registry, TInfo info, CancellationToken ct); diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 4c01c94f..8babef03 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -18,7 +18,7 @@ public interface IAssetRegistry : IDisposable bool WasDisposed { get; } bool IsMainThread { get; } IAssetRegistry? ParentRegistry { get; } - bool IsLocalRegistry => ParentRegistry is not null; + bool IsLocalRegistry { get; } CancellationToken Cancellation { get; } // is triggered when registry is disposed AssetHandle Load(in TInfo info, AssetPriority priority) From 269a919f174d83080c2522ea2f2d1a6b0a3bf942 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 26 Jun 2025 17:46:17 +0200 Subject: [PATCH 23/64] Add tests for invalid handle copies --- zzre.core.tests/TestAssetRegistry.cs | 26 +++++++++++++++++++++- zzre.core/assetregistry/AssetRegistry.cs | 27 ++++++++++------------- zzre.core/assetregistry/IAssetRegistry.cs | 1 - 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index b2ee9cbc..46bf2ab2 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -564,7 +564,7 @@ public void LoadLow_ThenGetSync(CancellationToken ct) CommonAssetChecks(global, handle, 1, asset); // check whether unnecessary low batch will break something - global.Update(); + global.Update(); CommonAssetChecks(global, handle, 1, asset); } @@ -1128,5 +1128,29 @@ public void Handle_Equality() 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()); + } + } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index a96ecad5..e88c5547 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -156,22 +156,19 @@ void IAssetRegistryInternal.DelRef(Guid assetId) } } - AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) => - assets.GetValueOrDefault(assetId)?.LoadLazy ?? NullAssetLoadLazy; - - public Task WaitForAll(IEnumerable assetHandles, CancellationToken ct) + AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) { - ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - if (assetHandles.Any(h => h.Registry != this && h.Registry != ParentRegistry)) - throw new ArgumentException("Cannot wait for assets from a foreign registry"); - return Task.WhenAll(assetHandles - .Select(id => - assets.GetValueOrDefault(id.AssetId) ?? - parentRegistry?.assets.GetValueOrDefault(id.AssetId)) - .Where(state => - state != null && - state.LoadLazy != NullAssetLoadLazy) - .Select(state => state!.LoadLazy.WithCancellation(ct))); + AssetState asset; + LockSemaphore(); + try + { + ObjectDisposedException.ThrowIf(!assets.TryGetValue(assetId, out asset!), nameof(IAssetHandle)); + } + finally + { + semaphore.Release(); + } + return asset.LoadLazy; } public AssetHandle Load(in TInfo info, AssetPriority priority) diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 8babef03..bf833bd5 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -26,7 +26,6 @@ AssetHandle Load(in TInfo info, AssetPriority priority) where TAsset : class, IAsset; void Update(); - Task WaitForAll(IEnumerable assets, CancellationToken ct); } internal interface IAssetRegistryInternal : IAssetRegistry From 987eeaa7d9a1e9a01dc0103889d42d48bef40bb4 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 26 Jun 2025 17:56:09 +0200 Subject: [PATCH 24/64] Remove unnecessary exception unpacking --- zzre.core/assetregistry/AssetRegistry.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index e88c5547..0c49a88f 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -192,14 +191,11 @@ public AssetHandle Load(in TInfo info, AssetPriority prio { handle.Get(); // checks main thread } - catch (Exception e) + catch { // the user does not get the handle, so there shouldn't be a refcount on the asset (this as IAssetRegistryInternal).DelRef(assetId); - if (e is AggregateException { InnerException: Exception inner }) - throw inner; - else - throw; + throw; } break; case AssetPriority.High: From 47f6c4107a3300c169ada98167e7bb95f1843717 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 26 Jun 2025 18:01:12 +0200 Subject: [PATCH 25/64] Add Unique asset test --- zzre.core.tests/TestAssetRegistry.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 46bf2ab2..6b128d9b 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -954,6 +954,26 @@ public void Local_LoadLocalFromLocal() 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) { From b45a977af5f87684b0ec9424db415c1fb68875b7 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 16 Jul 2025 08:16:07 +0200 Subject: [PATCH 26/64] Adapt Animation, Clump and Actor --- zzre.core.tests/TestAssetRegistry.cs | 11 +++ zzre.core/assetregistry/AssetHandle.cs | 3 +- zzre.core/assetregistry/IAsset.cs | 5 +- zzre.core/assetregistry/IAssetRegistry.cs | 3 +- zzre/assets/ActorAsset.cs | 96 ++++++++++++----------- zzre/assets/AnimationAsset.cs | 35 ++++----- zzre/assets/ClumpAsset.cs | 94 +++++----------------- 7 files changed, 106 insertions(+), 141 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 6b128d9b..934f52cc 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1172,5 +1172,16 @@ public void Handle_InvalidCopy_Access() Assert.That(() => invalidCopy.Get(), Throws.InstanceOf()); } + [Test] + public void Handle_Default_Dispose() + { + AssetHandle handle = default; + Assert.That(() => + { + handle.Dispose(); + handle.Dispose(); + }, Throws.Nothing); + } + } diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index d3abbf9c..df2fa568 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -114,7 +114,8 @@ public void Dispose() { if (wasDisposed) return; wasDisposed = true; - ((IAssetRegistryInternal)Registry).DelRef(AssetId); + if (Registry is not null && AssetId != default) + ((IAssetRegistryInternal)Registry).DelRef(AssetId); } public readonly TAsset? Asset diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 5015e009..646f24bc 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -15,9 +14,11 @@ public enum AssetLocality public interface IAsset : IDisposable { - public static abstract AssetLocality Locality { get; } + public static virtual AssetLocality Locality => AssetLocality.Global; public static virtual bool NeedsMainThreadDisposal => false; IAssetRegistry Registry { get; } + + void IDisposable.Dispose() { } } public readonly record struct AssetLoadResult( diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index bf833bd5..75a91f1a 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; using DotNext.Threading; namespace zzre; @@ -17,6 +15,7 @@ public interface IAssetRegistry : IDisposable { bool WasDisposed { get; } bool IsMainThread { get; } + ITagContainer DIContainer { get; } IAssetRegistry? ParentRegistry { get; } bool IsLocalRegistry { get; } CancellationToken Cancellation { get; } // is triggered when registry is disposed diff --git a/zzre/assets/ActorAsset.cs b/zzre/assets/ActorAsset.cs index fc7cafb4..a412dbdf 100644 --- a/zzre/assets/ActorAsset.cs +++ b/zzre/assets/ActorAsset.cs @@ -1,83 +1,89 @@ 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(); + } + } - 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, 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); - private (AssetHandle, AssetHandle[]) LoadSecondaryPart(ActorPartDescription part) - { - var clump = Registry.Load(ClumpAsset.Info.Actor(part.model), SecondaryPriority).As(); - 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); + return new AssetLoadResult(new ActorAsset(registry, info) + { + body = body, + wings = wings, + Description = description + }); } - private static void AddSecondaryHandles(AssetHandle[] handles, ref int outI, AssetHandle clump, AssetHandle[] animations) + private static Part LoadSecondaryPart(IAssetRegistry registry, ActorPartDescription part) { - handles[outI++] = clump; + var clump = registry.LoadActorClump(part.model, AssetPriority.High); + var animations = new AssetHandle[part.animations.Length]; for (int i = 0; i < animations.Length; i++) - handles[outI++] = animations[i]; + animations[i] = registry.LoadAnimation(part.animations[i].filename, AssetPriority.High); + return new(clump, animations); } - protected override void Unload() + private static void AddSecondaryHandles(Task[] handles, ref int outI, in Part part, CancellationToken ct) { - description = null; - bodyAnimations = []; - wingsAnimations = []; + handles[outI++] = part.ClumpHandle.GetAsync(ct).AsTask(); + foreach (var animHandle in part.AnimHandles) + handles[outI++] = animHandle.GetAsync(ct).AsTask(); } - protected override string ToStringInner() => $"Actor {info.Name}"; + public override string ToString() => $"Actor {info.Name}"; } diff --git a/zzre/assets/AnimationAsset.cs b/zzre/assets/AnimationAsset.cs index 210f0136..c556ec0a 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,32 @@ 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, 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 override string ToString() => $"Animation {info.Name}"; +} - protected override string ToStringInner() => $"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..da1c4eb6 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, 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); } From c77fb9b34be55b66059a9382034e42d6cf0bcd9e Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 16 Jul 2025 11:27:53 +0200 Subject: [PATCH 27/64] Adapt SamplerAsset, SoundAsset, TextureASset --- zzio.sln | 15 +++++ zzre.core/assetregistry/IAsset.cs | 2 - zzre/assets/ActorAsset.cs | 15 +++++ zzre/assets/AnimationAsset.cs | 2 + zzre/assets/SamplerAsset.cs | 61 ++++++++++---------- zzre/assets/SoundAsset.cs | 93 +++++++++++++------------------ zzre/assets/TextureAsset.cs | 87 +++++++++++------------------ zzre/assets/UIBitmapAsset.cs | 2 +- 8 files changed, 136 insertions(+), 141 deletions(-) diff --git a/zzio.sln b/zzio.sln index d8d7462e..64f4ecc4 100644 --- a/zzio.sln +++ b/zzio.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzio_dbsqlite", "zzio_dbsql EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzsc", "zzsc\zzsc.csproj", "{DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre", "zzre\zzre.csproj", "{B1A31FB7-7537-4407-8810-5FFB712FD87B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre.core", "zzre.core\zzre.core.csproj", "{CA61C447-0011-4FE7-A358-48EFF2709E68}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zzre.core.tests", "zzre.core.tests\zzre.core.tests.csproj", "{489E0003-E309-477B-B112-4ADDCDA99B2A}" @@ -102,6 +104,18 @@ Global {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x64.Build.0 = Release|Any CPU {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x86.ActiveCfg = Release|Any CPU {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B}.Release|x86.Build.0 = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x64.Build.0 = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Debug|x86.Build.0 = Debug|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x64.ActiveCfg = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x64.Build.0 = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x86.ActiveCfg = Release|Any CPU + {B1A31FB7-7537-4407-8810-5FFB712FD87B}.Release|x86.Build.0 = Release|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA61C447-0011-4FE7-A358-48EFF2709E68}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -147,6 +161,7 @@ Global {29A2E64F-8041-4D0E-B1BF-5E1A9BA17351} = {734E1888-6627-4A7F-8921-F7462F9CF686} {EB62CEEA-164C-4BC8-8CF9-87828B5E4C58} = {78DA8A29-9D93-4F5F-9420-47959E92360A} {DB4D2E5B-1E6E-4AAC-A1C2-1D97CFA09D9B} = {78DA8A29-9D93-4F5F-9420-47959E92360A} + {B1A31FB7-7537-4407-8810-5FFB712FD87B} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} {CA61C447-0011-4FE7-A358-48EFF2709E68} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} {489E0003-E309-477B-B112-4ADDCDA99B2A} = {734E1888-6627-4A7F-8921-F7462F9CF686} {BE8DA2D7-E8FC-4767-894F-CD760F90D5CF} = {1A98F664-A66E-44DF-8E3C-0618671E4AFC} diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index 646f24bc..e8dd8b86 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -17,8 +17,6 @@ public interface IAsset : IDisposable public static virtual AssetLocality Locality => AssetLocality.Global; public static virtual bool NeedsMainThreadDisposal => false; IAssetRegistry Registry { get; } - - void IDisposable.Dispose() { } } public readonly record struct AssetLoadResult( diff --git a/zzre/assets/ActorAsset.cs b/zzre/assets/ActorAsset.cs index a412dbdf..9a1dcff0 100644 --- a/zzre/assets/ActorAsset.cs +++ b/zzre/assets/ActorAsset.cs @@ -27,6 +27,8 @@ private readonly record struct Part( public void Dispose() { ClumpHandle.Dispose(); + foreach (var animHandle in AnimHandles ?? []) + animHandle.Dispose(); } } @@ -85,5 +87,18 @@ private static void AddSecondaryHandles(Task[] handles, ref int outI, in Part pa handles[outI++] = animHandle.GetAsync(ct).AsTask(); } + public void Dispose() + { + body.Dispose(); + wings.Dispose(); + body = wings = default; + } + 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/AnimationAsset.cs b/zzre/assets/AnimationAsset.cs index c556ec0a..7ae91900 100644 --- a/zzre/assets/AnimationAsset.cs +++ b/zzre/assets/AnimationAsset.cs @@ -37,6 +37,8 @@ static Task> IAsset.LoadAsync(IAssetRegistry registr return Task.FromResult(new AssetLoadResult(new AnimationAsset(registry, info, animation))); } + public void Dispose() { } + public override string ToString() => $"Animation {info.Name}"; } diff --git a/zzre/assets/SamplerAsset.cs b/zzre/assets/SamplerAsset.cs index 7c5a9b1a..75469ba3 100644 --- a/zzre/assets/SamplerAsset.cs +++ b/zzre/assets/SamplerAsset.cs @@ -1,26 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Text; +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, 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 +39,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..7b8b849e 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, 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..c1cb11c7 100644 --- a/zzre/assets/TextureAsset.cs +++ b/zzre/assets/TextureAsset.cs @@ -9,45 +9,40 @@ using zzre.rendering; using Texture = Veldrid.Texture; using PixelFormat = Veldrid.PixelFormat; +using System.Threading; 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; - - public Texture Texture => texture ?? - throw new InvalidOperationException("Asset was not yet loaded"); + private readonly Info info = info; - public TextureAsset(IAssetRegistry registry, Guid assetId, Info info) : base(registry, assetId) - { - path = info.FullPath; - } + public IAssetRegistry Registry { get; } = registry; + public Texture Texture { get; private set; } = null!; - protected override ValueTask> Load() + static Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) { - 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 +69,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 +119,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 +146,21 @@ 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(); - - private static void ApplyTextureToMaterial(AssetHandle handle, ref readonly ITexturedMaterial material) - { - var texture = handle.Get().Texture; - if (!material.WasDisposed) - material.Texture.Texture = texture; - } + AssetPriority priority) => + registry.Load(new(fullPath), priority); } diff --git a/zzre/assets/UIBitmapAsset.cs b/zzre/assets/UIBitmapAsset.cs index 30b02041..363828c7 100644 --- a/zzre/assets/UIBitmapAsset.cs +++ b/zzre/assets/UIBitmapAsset.cs @@ -42,7 +42,7 @@ public UIBitmapAsset(IAssetRegistry registry, Guid assetId, Info info, string? d DebugName = debugName ?? ($"UIBitmap {info.Name}" + (info.HasRawMask ? "" : " (Raw mask)")); } - // strictly speaking this is a workaround: waiting on global secondary assets + // strictly speaking this is a workaround: waiting on global secondary assets (the sampler) // 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) From b84df4d870330f7cb622888b145366732f89aab5 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 16 Jul 2025 14:11:28 +0200 Subject: [PATCH 28/64] Adapt UIBitmapAsset, UIPreloadAsset, UITileSheetAsset --- zzre/assets/TextureAsset.cs | 1 - zzre/assets/UIBitmapAsset.cs | 104 ++++++++++--------------- zzre/assets/UIPreloadAsset.cs | 131 ++++++++++++++------------------ zzre/assets/UITileSheetAsset.cs | 124 ++++++++++++++---------------- 4 files changed, 159 insertions(+), 201 deletions(-) diff --git a/zzre/assets/TextureAsset.cs b/zzre/assets/TextureAsset.cs index c1cb11c7..01822b34 100644 --- a/zzre/assets/TextureAsset.cs +++ b/zzre/assets/TextureAsset.cs @@ -6,7 +6,6 @@ using Veldrid; using zzio; using zzio.vfs; -using zzre.rendering; using Texture = Veldrid.Texture; using PixelFormat = Veldrid.PixelFormat; using System.Threading; diff --git a/zzre/assets/UIBitmapAsset.cs b/zzre/assets/UIBitmapAsset.cs index 363828c7..a6b72af4 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 (the sampler) - // 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, 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..78a6bdb0 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, 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..739ebd02 100644 --- a/zzre/assets/UITileSheetAsset.cs +++ b/zzre/assets/UITileSheetAsset.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Veldrid; using zzre.game; @@ -9,7 +8,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 +18,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, 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 +59,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); } From ed27a811a90a27e3ff80c06771bced1d4257ce85 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 17 Jul 2025 14:38:00 +0200 Subject: [PATCH 29/64] Add IAssetRegistry.Apply --- zzre.core.tests/TestAssetRegistry.cs | 340 ++++++++++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 166 ++++++++++- zzre.core/assetregistry/IAssetRegistry.cs | 3 + 3 files changed, 497 insertions(+), 12 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 934f52cc..6c9ba720 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -1183,5 +1184,344 @@ public void Handle_Default_Dispose() }, Throws.Nothing); } + [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()); + + 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); + + 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 + } } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 0c49a88f..d6d41a76 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -15,6 +15,7 @@ internal sealed class AssetState { public required bool NeedsMainThreadDisposal; public required AsyncLazy LoadLazy; + public required uint Tag; // used for apply actions to prevent triggering from revived assets public IDisposable? Asset { get @@ -47,6 +48,9 @@ public class AssetRegistry : IAssetRegistryInternal private readonly ILogger logger; private readonly int mainThreadId; private readonly AssetRegistry? parentRegistry; + private readonly Dictionary> applyActionCaster = []; + private List<(Guid assetId, uint tag, Type assetType, object action)> applyActions = [], applyActionsBackup = []; + private uint nextAssetTag = 0; public bool WasDisposed => cancellationSource.IsCancellationRequested; public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; @@ -81,6 +85,10 @@ public void Dispose() DisposeAssetState(asset); } assets.Clear(); + applyActions.Clear(); + applyActionsBackup.Clear(); + assetsToDispose.Writer.TryComplete(); + assetsToStart.Writer.TryComplete(); DisposeOldAssets(); // after current assets in case we add something into it (we shouldn't) semaphore.Dispose(); @@ -123,9 +131,7 @@ void IAssetRegistryInternal.AddRef(Guid assetId) LockSemaphore(); try { - var assetState = assets.GetValueOrDefault(assetId); - ObjectDisposedException.ThrowIf(assetState is null || assetState.RefCount <= 0, typeof(AssetState)); - assetState.RefCount++; + ObjectDisposedException.ThrowIf(!TryAddRefUnsafe(assetId), typeof(IAsset)); } finally { @@ -133,6 +139,17 @@ void IAssetRegistryInternal.AddRef(Guid assetId) } } + 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) { @@ -140,14 +157,7 @@ void IAssetRegistryInternal.DelRef(Guid assetId) LockSemaphore(); try { - 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); - } + DelRefUnsafe(assetId); } finally { @@ -155,6 +165,20 @@ void IAssetRegistryInternal.DelRef(Guid 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; @@ -242,7 +266,8 @@ public AssetHandle Load(in TInfo info, AssetPriority prio assetState = new() { NeedsMainThreadDisposal = TAsset.NeedsMainThreadDisposal, - LoadLazy = new(ct => LoadAsset(infoCopy, assetId)) + LoadLazy = new(ct => LoadAsset(infoCopy, assetId)), + Tag = unchecked(++nextAssetTag) }; assets[assetId] = assetState; return (assetId, assetState); @@ -312,10 +337,12 @@ public void Update(int maxLowPrioAssets) ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); if (!IsMainThread) throw new InvalidOperationException("Low batch scheduling is only allowed on the main thread"); + LockSemaphore(); try { DisposeOldAssets(); + for (int i = 0; i < maxLowPrioAssets && assetsToStart.Reader.TryRead(out var assetId); i++) { if (assets.TryGetValue(assetId, out var assetState) && @@ -323,11 +350,69 @@ public void Update(int maxLowPrioAssets) !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; + } + } + } + finally + { + semaphore.Release(); + } + + // 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 + LockSemaphore(); + try + { + foreach (var (assetId, _, _, _) in applyActionsBackup) + { + if (assetId != default) + DelRefUnsafe(assetId); + } } finally { semaphore.Release(); } + applyActionsBackup.Clear(); + + if (exceptions.Count > 0) + throw new AggregateException(exceptions); } private void DisposeOldAssets() @@ -337,4 +422,61 @@ private void DisposeOldAssets() while (assetsToDispose.Reader.TryRead(out var asset)) asset.Dispose(); } + + public void Apply(AssetHandle handle, Action> action) + where TAsset : class, IAsset + { + 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; + + LockSemaphore(); + try + { + 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)); + } + finally + { + semaphore.Release(); + } + + // Fast-path: no queueing + if (shouldBeExecutedNow) + { + try + { + action(new(this, handle.AssetId)); + } + finally + { + (this as IAssetRegistryInternal).DelRef(handle.AssetId); + } + } + } } diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 75a91f1a..aedc936f 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -24,6 +24,9 @@ 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; + void Update(); } From 4a3f8a5d62062927db1bd85f751d6c777e46a0a4 Mon Sep 17 00:00:00 2001 From: Helco Date: Mon, 21 Jul 2025 11:20:19 +0200 Subject: [PATCH 30/64] WIP adaption of ClumpMaterialAsset --- zzre.core/assetregistry/IAssetRegistry.cs | 2 + zzre.core/rendering/StandardTextures.cs | 2 +- zzre/assets/ClumpMaterialAsset.cs | 113 +++++++++++++++++----- zzre/assets/ModelMaterialAsset.cs | 7 +- 4 files changed, 98 insertions(+), 26 deletions(-) diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index aedc936f..0392ebf6 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -27,6 +27,8 @@ AssetHandle Load(in TInfo info, AssetPriority priority) void Apply(AssetHandle handle, Action> action) where TAsset : class, IAsset; + + void Update(); } 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/assets/ClumpMaterialAsset.cs b/zzre/assets/ClumpMaterialAsset.cs index 41666bfe..ad13220e 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,16 +18,15 @@ public sealed class ClumpMaterialAsset : ModelMaterialAsset new FilePath("resources/textures/worlds"), new FilePath("resources/textures/backdrops") ]; - protected override IReadOnlyList TextureBasePaths => ClumpTextureBasePaths; - public readonly record struct Info( - string? textureName, - SamplerDescription sampler, - MaterialVariant config, - StandardTextureKind? texturePlaceholder = null); + static AssetLocality IAsset.Locality => AssetLocality.Local; + static bool IAsset.NeedsMainThreadDisposal => true; // an Apply action wants to access Material - public static void Register() => - AssetInfoRegistry.Register(AssetLocality.Context); + public readonly record struct Info( + string? TextureName, + SamplerDescription Sampler, + MaterialVariant Variant, + StandardTextureKind? TexturePlaceholder = null); public readonly record struct MaterialVariant( ModelMaterial.BlendMode BlendMode = ModelMaterial.BlendMode.Opaque, @@ -46,26 +46,85 @@ public MaterialVariant(zzio.effect.EffectPartRenderMode renderMode, bool depthTe zzio.effect.EffectPartRenderMode.NormalBlend => ModelMaterial.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 : ""; } - private readonly MaterialVariant materialVariant; + private AssetHandle textureHandle; + private AssetHandle samplerHandle; + + public IAssetRegistry Registry { get; } = registry; + public ModelMaterial Material { get; private set; } = null!; - public ClumpMaterialAsset(IAssetRegistry registry, Guid assetId, Info info) - : base(registry, assetId, info.textureName, info.sampler, info.texturePlaceholder) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) { - materialVariant = info.config; + var diContainer = registry.DIContainer; + var material = new ModelMaterial(diContainer) + { + DebugName = $"ClumpMat {info.TextureName} {info.Variant}" + }; + SetMaterialVariant(diContainer, material, info.Variant); + + 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 (initalTexture, textureHandle) = await LoadTexture(registry, info.TextureName, info.TexturePlaceholder, ct); + material.Texture.Texture = initialTexture; + + return new(new ClumpMaterialAsset(registry) + { + samplerHandle = samplerHandle, + textureHandle = textureHandle, + Material = material + }); } - protected override void SetMaterialVariant(ModelMaterial material) + private static async Task<(Texture, AssetHandle)> LoadTexture( + IAssetRegistry registry, + , + string? textureName, + StandardTextureKind? placeholder, + CancellationToken ct) { - 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; + 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) + { + return (standardTextures.ByKind(placeholder!.Value), default); + } + else if (placeholder is null) + { + var handle = registry.LoadTexture(ClumpTextureBasePaths, textureName, AssetPriority.High); + var texture = await handle.GetAsync(ct); + return (texture.Texture, handle); + } + else + { + var handle = registry.LoadTexture(ClumpTextureBasePaths, textureName, AssetPriority.High); + registry.Apply(handle, h => ) + return (standardTextures.ByKind(placeholder.Value), handle); + } + } + + private static void SetMaterialVariant( + ITagContainer diContainer, + ModelMaterial material, + MaterialVariant materialVariant) + { + 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() { @@ -77,6 +136,14 @@ protected override void SetMaterialVariant(ModelMaterial material) if (materialVariant.HasFog && diContainer.TryGetTag>(out var fogParams)) material.FogParams.Buffer = fogParams.Buffer; } + + public void Dispose() + { + textureHandle.Dispose(); + samplerHandle.Dispose(); + Material?.Dispose(); + Material = null!; + } } partial class AssetExtensions diff --git a/zzre/assets/ModelMaterialAsset.cs b/zzre/assets/ModelMaterialAsset.cs index 9a54e416..6bc45a71 100644 --- a/zzre/assets/ModelMaterialAsset.cs +++ b/zzre/assets/ModelMaterialAsset.cs @@ -9,7 +9,7 @@ namespace zzre; -public abstract class ModelMaterialAsset : Asset +public abstract class ModelMaterialAsset(IAssetRegistry registry) : IAsset { private const string UseStandardTexture = "marker"; // Funatics never gave us this texture :( @@ -18,7 +18,10 @@ public abstract class ModelMaterialAsset : Asset private readonly StandardTextureKind? texturePlaceholder; private ModelMaterial? material; - public string DebugName { get; protected set; } + static AssetLocality IAsset.Locality => AssetLocality.Local; + + public IAssetRegistry Registry { get; } = registry; + public string DebugName { get; protected set; } = ""; public ModelMaterial Material => material ?? throw new InvalidOperationException("Asset was not yet loaded"); From 2afce5b70d45e47ab50e5df99a281217bef557ad Mon Sep 17 00:00:00 2001 From: Helco Date: Mon, 21 Jul 2025 12:06:19 +0200 Subject: [PATCH 31/64] Add IAssetRegistry.TryGet --- zzre.core.tests/TestAssetRegistry.cs | 117 ++++++++++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 32 +++++- zzre.core/assetregistry/IAssetRegistry.cs | 3 +- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 6c9ba720..0e751d6c 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1524,4 +1524,121 @@ public async Task Apply_ErrorAfterLoad(CancellationToken ct) 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); + 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); + } } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index d6d41a76..e6e6da09 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -16,6 +16,7 @@ internal sealed class AssetState 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 @@ -50,7 +51,7 @@ public class AssetRegistry : IAssetRegistryInternal private readonly AssetRegistry? parentRegistry; private readonly Dictionary> applyActionCaster = []; private List<(Guid assetId, uint tag, Type assetType, object action)> applyActions = [], applyActionsBackup = []; - private uint nextAssetTag = 0; + private uint nextAssetTag; public bool WasDisposed => cancellationSource.IsCancellationRequested; public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; @@ -267,7 +268,8 @@ public AssetHandle Load(in TInfo info, AssetPriority prio { NeedsMainThreadDisposal = TAsset.NeedsMainThreadDisposal, LoadLazy = new(ct => LoadAsset(infoCopy, assetId)), - Tag = unchecked(++nextAssetTag) + Tag = unchecked(++nextAssetTag), + AssetType = typeof(TAsset) }; assets[assetId] = assetState; return (assetId, assetState); @@ -329,6 +331,32 @@ void CheckRegistryDisposal() } } + public bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset + { + ObjectDisposedException.ThrowIf(WasDisposed, typeof(AssetRegistry)); + if (ParentRegistry?.TryGet(assetId, out handle) is true) + return true; + + handle = default; + LockSemaphore(); + try + { + 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; + } + finally + { + semaphore.Release(); + } + } + public void Update() => Update(MaxLowPriorityAssetsPerFrame); diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 0392ebf6..2e574910 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -27,7 +27,8 @@ AssetHandle Load(in TInfo info, AssetPriority priority) void Apply(AssetHandle handle, Action> action) where TAsset : class, IAsset; - + bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset; void Update(); } From bc76bed64c7122dbd5148581c69d2a1eb1692f8d Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 22 Jul 2025 10:32:22 +0200 Subject: [PATCH 32/64] Adapt ClumpMaterialAsset --- zzre.core/assetregistry/AssetRegistry.cs | 2 +- zzre.core/assetregistry/IAsset.cs | 2 +- zzre/assets/ActorAsset.cs | 2 +- zzre/assets/AnimationAsset.cs | 2 +- zzre/assets/ClumpAsset.cs | 2 +- zzre/assets/ClumpMaterialAsset.cs | 123 ++------------------ zzre/assets/CommonMaterialAsset.cs | 94 +++++++++++++++ zzre/assets/ModelMaterialAsset.cs | 141 ----------------------- zzre/assets/SamplerAsset.cs | 5 +- zzre/assets/SoundAsset.cs | 2 +- zzre/assets/TextureAsset.cs | 2 +- zzre/assets/UIBitmapAsset.cs | 2 +- zzre/assets/UIPreloadAsset.cs | 2 +- zzre/assets/UITileSheetAsset.cs | 5 +- zzre/materials/ModelMaterial.cs | 56 +++++++++ 15 files changed, 177 insertions(+), 265 deletions(-) create mode 100644 zzre/assets/CommonMaterialAsset.cs delete mode 100644 zzre/assets/ModelMaterialAsset.cs diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index e6e6da09..ed9fab99 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -295,7 +295,7 @@ private async Task LoadAsset(TInfo info, Guid assetI // Due to AsyncLazy we can flow exceptions outside this method // Load asset and secondary assets - var asset = (await TAsset.LoadAsync(this, info, Cancellation)).Asset; + var asset = (await TAsset.LoadAsync(this, assetId, info, Cancellation)).Asset; Debug.Assert(asset.Registry == this); CheckRegistryDisposal(); diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index e8dd8b86..b8e31c4a 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -27,7 +27,7 @@ public interface IAsset : IAsset where TInfo : struct, IEquatable { static virtual Guid InfoToAssetId(in TInfo info) => GeneralInfoToGuid(info); - static abstract Task> LoadAsync(IAssetRegistry registry, TInfo info, CancellationToken ct); + static abstract Task> LoadAsync(IAssetRegistry registry, Guid assetId, TInfo info, CancellationToken ct); private static readonly object generalInfoLock = new(); private static readonly Dictionary generalInfoToGuid = []; diff --git a/zzre/assets/ActorAsset.cs b/zzre/assets/ActorAsset.cs index 9a1dcff0..b3e25bcd 100644 --- a/zzre/assets/ActorAsset.cs +++ b/zzre/assets/ActorAsset.cs @@ -44,7 +44,7 @@ public void Dispose() public ReadOnlySpan BodyAnimations => body.Animations; public ReadOnlySpan WingsAnimations => wings.Animations; - static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { var resourcePool = registry.DIContainer.GetTag(); diff --git a/zzre/assets/AnimationAsset.cs b/zzre/assets/AnimationAsset.cs index 7ae91900..e2a7cf2a 100644 --- a/zzre/assets/AnimationAsset.cs +++ b/zzre/assets/AnimationAsset.cs @@ -28,7 +28,7 @@ private AnimationAsset(IAssetRegistry registry, Info info, SkeletalAnimation ani Animation = animation; } - static Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { var resourcePool = registry.DIContainer.GetTag(); using var stream = resourcePool.FindAndOpen(info.FullPath) ?? diff --git a/zzre/assets/ClumpAsset.cs b/zzre/assets/ClumpAsset.cs index da1c4eb6..ce2e915c 100644 --- a/zzre/assets/ClumpAsset.cs +++ b/zzre/assets/ClumpAsset.cs @@ -43,7 +43,7 @@ public void Dispose() Mesh = null!; } - static Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { var mesh = new ClumpMesh(registry.DIContainer, info.FullPath); return Task.FromResult(new AssetLoadResult(new ClumpAsset(registry, info, mesh))); diff --git a/zzre/assets/ClumpMaterialAsset.cs b/zzre/assets/ClumpMaterialAsset.cs index ad13220e..09488cde 100644 --- a/zzre/assets/ClumpMaterialAsset.cs +++ b/zzre/assets/ClumpMaterialAsset.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Veldrid; using zzio; -using zzio.rwbs; using zzre.materials; using zzre.rendering; using static zzre.ClumpMaterialAsset; @@ -25,55 +24,31 @@ public sealed class ClumpMaterialAsset(IAssetRegistry registry) : IAsset public readonly record struct Info( string? TextureName, SamplerDescription Sampler, - MaterialVariant Variant, + ModelMaterial.Variant Variant, StandardTextureKind? TexturePlaceholder = null); - 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) { } - - private static ModelMaterial.BlendMode BlendFromRenderMode(zzio.effect.EffectPartRenderMode renderMode) => renderMode switch - { - 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}") - }; - - 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 : ""; - } - private AssetHandle textureHandle; private AssetHandle samplerHandle; public IAssetRegistry Registry { get; } = registry; public ModelMaterial Material { get; private set; } = null!; - static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid assetId, Info info, CancellationToken ct) { var diContainer = registry.DIContainer; var material = new ModelMaterial(diContainer) { DebugName = $"ClumpMat {info.TextureName} {info.Variant}" }; - SetMaterialVariant(diContainer, material, info.Variant); + material.Apply(info.Variant, 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 (initalTexture, textureHandle) = await LoadTexture(registry, info.TextureName, info.TexturePlaceholder, ct); + var (initialTexture, textureHandle) = await AssetExtensions.LoadTextureForMaterial( + registry, ClumpTextureBasePaths, assetId, info.TextureName, info.TexturePlaceholder, ct); material.Texture.Texture = initialTexture; return new(new ClumpMaterialAsset(registry) @@ -84,59 +59,6 @@ static async Task> IAsset.LoadAsync(IAssetRegistry r }); } - private static async Task<(Texture, AssetHandle)> LoadTexture( - IAssetRegistry registry, - , - string? textureName, - StandardTextureKind? placeholder, - 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) - { - return (standardTextures.ByKind(placeholder!.Value), default); - } - else if (placeholder is null) - { - var handle = registry.LoadTexture(ClumpTextureBasePaths, textureName, AssetPriority.High); - var texture = await handle.GetAsync(ct); - return (texture.Texture, handle); - } - else - { - var handle = registry.LoadTexture(ClumpTextureBasePaths, textureName, AssetPriority.High); - registry.Apply(handle, h => ) - return (standardTextures.ByKind(placeholder.Value), handle); - } - } - - private static void SetMaterialVariant( - ITagContainer diContainer, - ModelMaterial material, - MaterialVariant materialVariant) - { - 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; - } - public void Dispose() { textureHandle.Dispose(); @@ -151,32 +73,11 @@ 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(); - - public static AssetHandle LoadClumpMaterial(this IAssetRegistry registry, - RWMaterial rwMaterial, - MaterialVariant config, - StandardTextureKind? texturePlaceholder = null) - { - 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(); - } + ModelMaterial.Variant config, + StandardTextureKind? texturePlaceholder = null, + AssetPriority priority = AssetPriority.Synchronous) => + registry.Load( + new(textureName, sampler, config, texturePlaceholder), + priority + ); } diff --git a/zzre/assets/CommonMaterialAsset.cs b/zzre/assets/CommonMaterialAsset.cs new file mode 100644 index 00000000..001f245c --- /dev/null +++ b/zzre/assets/CommonMaterialAsset.cs @@ -0,0 +1,94 @@ +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, + 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, AssetPriority.High); + var texture = await handle.GetAsync(ct); + return (texture.Texture, handle); + } + else + { + var handle = registry.LoadTexture(texturePaths, textureName, AssetPriority.High); + 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/ModelMaterialAsset.cs b/zzre/assets/ModelMaterialAsset.cs deleted file mode 100644 index 6bc45a71..00000000 --- a/zzre/assets/ModelMaterialAsset.cs +++ /dev/null @@ -1,141 +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(IAssetRegistry registry) : IAsset -{ - 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; - - static AssetLocality IAsset.Locality => AssetLocality.Local; - - public IAssetRegistry Registry { get; } = registry; - 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 75469ba3..cdb510c3 100644 --- a/zzre/assets/SamplerAsset.cs +++ b/zzre/assets/SamplerAsset.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using System.Threading; using System.Threading.Tasks; using Veldrid; @@ -11,7 +12,7 @@ public sealed class SamplerAsset(IAssetRegistry registry) : IAsset> IAsset.LoadAsync(IAssetRegistry registry, SamplerDescription info, CancellationToken ct) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, SamplerDescription info, CancellationToken ct) { var resourceFactory = registry.DIContainer.GetTag(); var sampler = resourceFactory.CreateSampler(info); diff --git a/zzre/assets/SoundAsset.cs b/zzre/assets/SoundAsset.cs index 7b8b849e..f38f5f9d 100644 --- a/zzre/assets/SoundAsset.cs +++ b/zzre/assets/SoundAsset.cs @@ -24,7 +24,7 @@ public sealed class SoundAsset(IAssetRegistry registry, SoundAsset.Info info) : public IAssetRegistry Registry { get; } = registry; public uint Buffer { get; private set; } - static Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid __, Info info, CancellationToken ct) { var diContainer = registry.DIContainer; uint buffer = 0; // a valid but unusable state diff --git a/zzre/assets/TextureAsset.cs b/zzre/assets/TextureAsset.cs index 01822b34..341124c9 100644 --- a/zzre/assets/TextureAsset.cs +++ b/zzre/assets/TextureAsset.cs @@ -24,7 +24,7 @@ public Info(string fullPath) : this(new FilePath(fullPath)) { } public IAssetRegistry Registry { get; } = registry; public Texture Texture { get; private set; } = null!; - static Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { var resourcePool = registry.DIContainer.GetTag(); using var textureStream = resourcePool.FindAndOpen(info.FullPath) ?? diff --git a/zzre/assets/UIBitmapAsset.cs b/zzre/assets/UIBitmapAsset.cs index a6b72af4..63866a99 100644 --- a/zzre/assets/UIBitmapAsset.cs +++ b/zzre/assets/UIBitmapAsset.cs @@ -33,7 +33,7 @@ public sealed class UIBitmapAsset(IAssetRegistry registry, UIBitmapAsset.Info in public UIMaterial Material { get; private set; } = null!; public Vector2 Size => new(Material.MainTexture.Texture!.Width, Material.MainTexture.Texture!.Height); - static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + 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); diff --git a/zzre/assets/UIPreloadAsset.cs b/zzre/assets/UIPreloadAsset.cs index 78a6bdb0..fecbca70 100644 --- a/zzre/assets/UIPreloadAsset.cs +++ b/zzre/assets/UIPreloadAsset.cs @@ -48,7 +48,7 @@ public static readonly UITileSheetAsset.Info static AssetLocality IAsset.Locality => AssetLocality.Local; // the bitmaps are local public IAssetRegistry Registry { get; } = registry; - static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { AssetHandle Preload(out AssetHandle handle, UITileSheetAsset.Info info) => handle = registry.LoadUITileSheet(info, AssetPriority.High); diff --git a/zzre/assets/UITileSheetAsset.cs b/zzre/assets/UITileSheetAsset.cs index 739ebd02..2acd6cfb 100644 --- a/zzre/assets/UITileSheetAsset.cs +++ b/zzre/assets/UITileSheetAsset.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Veldrid; using zzre.game; @@ -32,7 +33,7 @@ public readonly record struct Info( public UIMaterial Material { get; private set; } = null!; public TileSheet TileSheet { get; private set; } = null!; - static async Task> IAsset.LoadAsync(IAssetRegistry registry, Info info, CancellationToken ct) + static async Task> IAsset.LoadAsync(IAssetRegistry registry, Guid _, Info info, CancellationToken ct) { var samplerHandle = registry.LoadSampler(SamplerDescription, AssetPriority.High); var isFont = (info.LineHeight ?? info.LineOffset ?? info.CharSpacing) is not null; diff --git a/zzre/materials/ModelMaterial.cs b/zzre/materials/ModelMaterial.cs index ae25de1e..6e6fd0a0 100644 --- a/zzre/materials/ModelMaterial.cs +++ b/zzre/materials/ModelMaterial.cs @@ -107,6 +107,62 @@ public ModelMaterial(ITagContainer diContainer) : base(diContainer, "model") DepthWrite = true; DepthTest = true; } + + public void Apply(in Variant variant, ITagContainer diContainer) + { + if (!diContainer.TryGetTag(out UniformBuffer fogParams)) + fogParams = null!; + Apply(variant, fogParams); + } + + public void Apply(in Variant variant, UniformBuffer? fogParams = null) + { + IsInstanced = variant.IsInstanced; + IsSkinned = variant.IsSkinned; + Blend = variant.BlendMode; + DepthWrite = variant.DepthWrite; + DepthTest = variant.DepthTest; + HasEnvMap = variant.HasEnvMap; + HasTexShift = variant.HasTexShift; + HasFog = variant.HasFog; + + Factors.Ref = new() + { + textureFactor = 1f, + vertexColorFactor = 1f, + tintFactor = 1f, + alphaReference = 0.082352944f + }; + if (variant.HasFog && fogParams is not null) + 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 From f7912fc53d7ffe82e94d8323ac38fd765976d9af Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 23 Jul 2025 10:32:27 +0200 Subject: [PATCH 33/64] Adapt all material assets --- zzre/assets/ActorMaterialAsset.cs | 109 +++++++++++++++------- zzre/assets/ClumpMaterialAsset.cs | 18 +++- zzre/assets/CommonMaterialAsset.cs | 5 +- zzre/assets/EffectMaterialAsset.cs | 140 +++++++++++++---------------- zzre/assets/WorldMaterialAsset.cs | 99 +++++++++++++------- zzre/materials/ModelMaterial.cs | 20 ++--- 6 files changed, 240 insertions(+), 151 deletions(-) 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/ClumpMaterialAsset.cs b/zzre/assets/ClumpMaterialAsset.cs index 09488cde..eca520ea 100644 --- a/zzre/assets/ClumpMaterialAsset.cs +++ b/zzre/assets/ClumpMaterialAsset.cs @@ -18,6 +18,14 @@ public sealed class ClumpMaterialAsset(IAssetRegistry registry) : IAsset new FilePath("resources/textures/backdrops") ]; + 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 @@ -40,7 +48,7 @@ static async Task> IAsset.LoadAsync(IAssetRegistry r { DebugName = $"ClumpMat {info.TextureName} {info.Variant}" }; - material.Apply(info.Variant, diContainer); + material.Apply(info.Variant, ModelFactors, diContainer); var camera = diContainer.GetTag(); material.Projection.BufferRange = camera.ProjectionRange; @@ -48,7 +56,13 @@ static async Task> IAsset.LoadAsync(IAssetRegistry r 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, ct); + registry, + ClumpTextureBasePaths, + assetId, + info.TextureName, + info.TexturePlaceholder, + AssetPriority.High, + ct); material.Texture.Texture = initialTexture; return new(new ClumpMaterialAsset(registry) diff --git a/zzre/assets/CommonMaterialAsset.cs b/zzre/assets/CommonMaterialAsset.cs index 001f245c..1368e021 100644 --- a/zzre/assets/CommonMaterialAsset.cs +++ b/zzre/assets/CommonMaterialAsset.cs @@ -22,6 +22,7 @@ partial class AssetExtensions Guid assetId, string? textureName, StandardTextureKind? placeholder, + AssetPriority priority, CancellationToken ct) { var standardTextures = registry.DIContainer.GetTag(); @@ -33,13 +34,13 @@ partial class AssetExtensions } else if (placeholder is null) { - var handle = registry.LoadTexture(texturePaths, textureName, AssetPriority.High); + var handle = registry.LoadTexture(texturePaths, textureName, priority); var texture = await handle.GetAsync(ct); return (texture.Texture, handle); } else { - var handle = registry.LoadTexture(texturePaths, textureName, AssetPriority.High); + var handle = registry.LoadTexture(texturePaths, textureName, priority); registry.Apply(handle, h => { if (registry.TryGet(assetId, out var materialHandle) && 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/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/materials/ModelMaterial.cs b/zzre/materials/ModelMaterial.cs index 6e6fd0a0..fdd8082c 100644 --- a/zzre/materials/ModelMaterial.cs +++ b/zzre/materials/ModelMaterial.cs @@ -108,14 +108,14 @@ public ModelMaterial(ITagContainer diContainer) : base(diContainer, "model") DepthTest = true; } - public void Apply(in Variant variant, ITagContainer diContainer) + public void Apply(in Variant variant, in ModelFactors? factors, ITagContainer diContainer) { if (!diContainer.TryGetTag(out UniformBuffer fogParams)) fogParams = null!; - Apply(variant, fogParams); + Apply(variant, factors, fogParams); } - public void Apply(in Variant variant, UniformBuffer? fogParams = null) + public void Apply(in Variant variant, in ModelFactors? factors = null, UniformBuffer? fogParams = null) { IsInstanced = variant.IsInstanced; IsSkinned = variant.IsSkinned; @@ -124,17 +124,15 @@ public void Apply(in Variant variant, UniformBuffer? fogParams = null DepthTest = variant.DepthTest; HasEnvMap = variant.HasEnvMap; HasTexShift = variant.HasTexShift; - HasFog = variant.HasFog; - Factors.Ref = new() - { - textureFactor = 1f, - vertexColorFactor = 1f, - tintFactor = 1f, - alphaReference = 0.082352944f - }; + 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( From 9f43ab52ef69255f3c8068872b2a4f9423b522cb Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 23 Jul 2025 16:31:01 +0200 Subject: [PATCH 34/64] Adapt EffectCombinerAsset and WorldAsset --- zzre/assets/EffectCombinerAsset.cs | 57 +++++++++++------------------- zzre/assets/WorldAsset.cs | 44 ++++++++++------------- 2 files changed, 39 insertions(+), 62 deletions(-) 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/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); } From b312903f1e306adff146ef9503fc6daba79e710f Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 24 Jul 2025 09:26:48 +0200 Subject: [PATCH 35/64] AssetLoadPriority -> AssetPriority --- zzre.core.tests/TestAssetRegistry.cs | 8 +- zzre/game/DuelGame.cs | 2 +- zzre/game/messages/LoadActor.cs | 2 +- zzre/game/messages/LoadModel.cs | 4 +- zzre/game/messages/sound/SpawnSample.cs | 4 +- zzre/game/systems/WorldRendererSystem.cs | 4 +- zzre/game/systems/animal/Animal.cs | 2 +- zzre/game/systems/animal/CollectionFairy.cs | 2 +- zzre/game/systems/effect/EffectCombiner.cs | 2 +- zzre/game/systems/effect/ModelEmitter.cs | 2 +- zzre/game/systems/effect/Sound.cs | 2 +- .../systems/fairy/OverworldFairySpawner.cs | 2 +- zzre/game/systems/model/BackdropLoader.cs | 2 +- zzre/game/systems/model/ModelLoader.cs | 10 +-- zzre/game/systems/npc/NPCScript.cs | 2 +- zzre/game/systems/player/PlayerSpawner.cs | 2 +- zzre/game/systems/sound/AmbientSounds.cs | 2 +- zzre/game/systems/sound/SceneSamples.cs | 2 +- zzre/game/uibuilder/UIBuilder.cs | 2 +- zzre/tools/ActorEditor.Part.cs | 4 +- zzre/tools/ModelViewer.cs | 76 +++++++++---------- zzre/tools/sceneeditor/SceneEditor.FOModel.cs | 4 +- zzre/tools/sceneeditor/SceneEditor.Model.cs | 4 +- 23 files changed, 73 insertions(+), 73 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 0e751d6c..1ed8fafa 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -65,7 +65,7 @@ private class GlobalTestAsset : ITestAsset public IAssetRegistry Registry { get; init; } public TestInfo Info { get; init; } - public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); public void Dispose() @@ -83,7 +83,7 @@ private class GlobalMTDTestAsset : ITestAsset public IAssetRegistry Registry { get; init; } public TestInfo Info { get; init; } - public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); public void Dispose() @@ -100,7 +100,7 @@ private class LocalTestAsset : ITestAsset public IAssetRegistry Registry { get; init; } public TestInfo Info { get; init; } - public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); public void Dispose() @@ -117,7 +117,7 @@ private class UniqueTestAsset : ITestAsset public IAssetRegistry Registry { get; init; } public TestInfo Info { get; init; } - public static Task> LoadAsync(IAssetRegistry registry, TestInfo info, CancellationToken ct) + public static Task> LoadAsync(IAssetRegistry registry, Guid _, TestInfo info, CancellationToken ct) => TestInfo.LoadAsync(registry, info, ct); public void Dispose() 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/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/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/EffectCombiner.cs b/zzre/game/systems/effect/EffectCombiner.cs index bdc6ac25..8159a593 100644 --- a/zzre/game/systems/effect/EffectCombiner.cs +++ b/zzre/game/systems/effect/EffectCombiner.cs @@ -74,7 +74,7 @@ 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); + assetRegistry.LoadEffectCombiner(entity, msg.FullPath, AssetPriority.Synchronous); var effect = entity.Get(); entity.Set(new components.effect.CombinerPlayback( duration: effect.isLooping ? float.PositiveInfinity : effect.Duration, diff --git a/zzre/game/systems/effect/ModelEmitter.cs b/zzre/game/systems/effect/ModelEmitter.cs index 08f21e48..220fb64d 100644 --- a/zzre/game/systems/effect/ModelEmitter.cs +++ b/zzre/game/systems/effect/ModelEmitter.cs @@ -46,7 +46,7 @@ protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzi assetRegistry.LoadModel(entity, data.texName, - AssetLoadPriority.Low, + AssetPriority.Low, new(data.renderMode, playback.DepthTest), StandardTextureKind.Clear); entity.Set(); 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..9b8a340a 100644 --- a/zzre/game/systems/model/BackdropLoader.cs +++ b/zzre/game/systems/model/BackdropLoader.cs @@ -92,7 +92,7 @@ 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.LoadBackdrop(entity, name, AssetPriority.Synchronous, materialVariant, StandardTextureKind.Clear); return entity; } diff --git a/zzre/game/systems/model/ModelLoader.cs b/zzre/game/systems/model/ModelLoader.cs index 285518d8..5612cb30 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,7 +131,7 @@ 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 { @@ -185,7 +185,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/uibuilder/UIBuilder.cs b/zzre/game/uibuilder/UIBuilder.cs index e8ee2035..475efea8 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 UIPreloadAsset.Info(), AssetPriority.High).As(); } protected override void DisposeManaged() diff --git a/zzre/tools/ActorEditor.Part.cs b/zzre/tools/ActorEditor.Part.cs index 2f21efdb..1c4c0d43 100644 --- a/zzre/tools/ActorEditor.Part.cs +++ b/zzre/tools/ActorEditor.Part.cs @@ -43,7 +43,7 @@ public Part(ITagContainer diContainer, string modelName, (AnimationType type, st 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.Load(new ClumpAsset.Info(modelPath), AssetPriority.Synchronous); AddDisposable(meshHandle); mesh = meshHandle.Get().Mesh; @@ -70,7 +70,7 @@ void LinkTransformsFor(IStandardTransformMaterial material) 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); + AssetPriority.Synchronous, material, StandardTextureKind.Error); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); if (textureHandle.HasValue) AddDisposable(textureHandle.Value); diff --git a/zzre/tools/ModelViewer.cs b/zzre/tools/ModelViewer.cs index 6fc46aff..a3df0fb6 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,7 +37,7 @@ 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; @@ -50,7 +50,7 @@ private enum CoarseCollisionMode 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,11 +162,11 @@ 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 ClumpAsset.Info(resource.Path), AssetPriority.Synchronous); assetHandles.Add(meshHandle); mesh = meshHandle.Get().Mesh; @@ -177,7 +177,7 @@ private void LoadModelNow(IResource resource) 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); + AssetPriority.Synchronous, material, StandardTextureKind.Error); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); if (textureHandle.HasValue) assetHandles.Add(textureHandle.Value); @@ -244,8 +244,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 +360,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 +384,7 @@ private void HandleCollisionContent() { ImGui.Text("No model loaded"); return; - } + } HandleCoarseCollisionContent(); if (collider == null) { diff --git a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs index 792d83a0..967175de 100644 --- a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs +++ b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs @@ -49,7 +49,7 @@ 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.Load(ClumpAsset.Info.Model(sceneModel.filename), AssetPriority.Synchronous).As(); mesh = meshHandle.Get().Mesh; if (mesh.IsEmpty) { @@ -62,7 +62,7 @@ public FOModel(ITagContainer diContainer, zzio.scn.FOModel sceneModel) 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.LoadTexture(textureBasePaths, rwTextureName.value, AssetPriority.Synchronous, material); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); materialHandles.Add(textureHandle); materialHandles.Add(samplerHandle); diff --git a/zzre/tools/sceneeditor/SceneEditor.Model.cs b/zzre/tools/sceneeditor/SceneEditor.Model.cs index 76a04421..04d089e9 100644 --- a/zzre/tools/sceneeditor/SceneEditor.Model.cs +++ b/zzre/tools/sceneeditor/SceneEditor.Model.cs @@ -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.Load(ClumpAsset.Info.Model(sceneModel.filename), AssetPriority.Synchronous).As(); mesh = meshHandle.Get().Mesh; if (mesh.IsEmpty) { @@ -65,7 +65,7 @@ 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.LoadTexture(textureBasePaths, rwTextureName.value, AssetPriority.Synchronous, material); var samplerHandle = assetRegistry.LoadSampler(SamplerDescription.Linear); materialHandles.Add(textureHandle); materialHandles.Add(samplerHandle); From faa28f2e54fde1312211d103a69dcafb3b5ddbc5 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 24 Jul 2025 09:27:44 +0200 Subject: [PATCH 36/64] Remove NullDisposable from zzre project --- zzre/Program.Remotery.cs | 7 ------- 1 file changed, 7 deletions(-) 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 From 64e2d2cfbc8a6f07ea0d20cc6053bc3ed42baaf7 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 24 Jul 2025 15:08:16 +0200 Subject: [PATCH 37/64] Shallow fixes for many tools --- zzre.core/assetregistry/AssetRegistry.cs | 9 ++-- zzre/assets/TextureAsset.cs | 25 ++++++++- zzre/tools/ModelViewer.cs | 13 ++--- zzre/tools/WorldViewer.cs | 52 +++++++++---------- zzre/tools/effecteditor/EffectEditor.cs | 10 ++-- zzre/tools/sceneeditor/SceneEditor.FOModel.cs | 20 +++---- zzre/tools/sceneeditor/SceneEditor.Model.cs | 15 +++--- zzre/tools/sceneeditor/SceneEditor.cs | 10 ++-- 8 files changed, 92 insertions(+), 62 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index ed9fab99..3a6c6df2 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -48,7 +48,6 @@ public class AssetRegistry : IAssetRegistryInternal private readonly SemaphoreSlim semaphore = new(1, 1); private readonly ILogger logger; private readonly int mainThreadId; - private readonly AssetRegistry? parentRegistry; private readonly Dictionary> applyActionCaster = []; private List<(Guid assetId, uint tag, Type assetType, object action)> applyActions = [], applyActionsBackup = []; private uint nextAssetTag; @@ -56,17 +55,17 @@ public class AssetRegistry : IAssetRegistryInternal public bool WasDisposed => cancellationSource.IsCancellationRequested; public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; public bool IsLocalRegistry => ParentRegistry is not null; - public IAssetRegistry? ParentRegistry => parentRegistry; + public IAssetRegistry? ParentRegistry { get; } public CancellationToken Cancellation => cancellationSource.Token; public ITagContainer DIContainer { get; } - public AssetRegistry(ITagContainer diContainer, AssetRegistry? parent = null, string? debugName = null) + public AssetRegistry(ITagContainer diContainer, IAssetRegistry? parent = null, string? debugName = null) { DIContainer = diContainer; 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; + ParentRegistry = parent; logger = // ILogger is optional, as well as the log prefix !diContainer.TryGetTag(out var parentLogger) ? Logger.None : string.IsNullOrEmpty(debugName) ? diContainer.GetLoggerFor() @@ -203,7 +202,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio 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); + return ParentRegistry!.Load(info, priority); var (assetId, assetState) = GetOrCreateAssetState(info); var handle = new AssetHandle(this, assetId); diff --git a/zzre/assets/TextureAsset.cs b/zzre/assets/TextureAsset.cs index 341124c9..33dd5e59 100644 --- a/zzre/assets/TextureAsset.cs +++ b/zzre/assets/TextureAsset.cs @@ -1,14 +1,16 @@ 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; -using System.Threading; namespace zzre; @@ -162,4 +164,25 @@ public static AssetHandle LoadTexture(this IAssetRegistry registry FilePath fullPath, AssetPriority priority) => registry.Load(new(fullPath), priority); + + public static AssetHandle? TryLoadTextureForMaterial(this IAssetRegistry registry, + IReadOnlyList texturePaths, + RWTexture rwTexture, + ITexturedMaterial material, + StandardTextureKind? placeholder = null) + { + 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/tools/ModelViewer.cs b/zzre/tools/ModelViewer.cs index a3df0fb6..28d80044 100644 --- a/zzre/tools/ModelViewer.cs +++ b/zzre/tools/ModelViewer.cs @@ -43,7 +43,7 @@ private enum CoarseCollisionMode 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; @@ -166,18 +166,19 @@ private void LoadModelNow(IResource resource) boundingRenderer.Clear(); coarseCollisionMode = default; - var meshHandle = assetRegistry.Load(new ClumpAsset.Info(resource.Path), AssetPriority.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, - AssetPriority.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); diff --git a/zzre/tools/WorldViewer.cs b/zzre/tools/WorldViewer.cs index 9bda224a..9a8f96af 100644 --- a/zzre/tools/WorldViewer.cs +++ b/zzre/tools/WorldViewer.cs @@ -34,7 +34,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 +49,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 +150,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)); + .AddTag(assetRegistry = new AssetRegistry(localDiContainer, globalAssetRegistry, "WorldViewer")); AssetRegistry.SubscribeAt(ecsWorld); - assetRegistry.DelayDisposals = false; worldRenderer = new(localDiContainer); AddDisposable(worldRenderer); } @@ -541,29 +541,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 +589,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..2e5ab489 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)); + diContainer.AddTag(assetRegistry = new AssetRegistry( + diContainer, + diContainer.GetTag(), + "EffectEditor")); AssetRegistry.SubscribeAt(ecsWorld); - assetRegistry.DelayDisposals = false; 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 967175de..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), AssetPriority.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, AssetPriority.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 04d089e9..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), AssetPriority.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, AssetPriority.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..a4289a40 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; + AssetRegistry.SubscribeAt(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; From deb88253ed1d81bb145d0a1b41b8f16be2f02c34 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 25 Jul 2025 08:31:04 +0200 Subject: [PATCH 38/64] Add generic AssetHandle --- zzre.core.tests/TestAssetRegistry.cs | 119 ++++++++++++++ zzre.core/assetregistry/AssetHandle.cs | 192 ++++------------------ zzre.core/assetregistry/AssetHandleT.cs | 129 +++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 15 ++ zzre.core/assetregistry/IAssetRegistry.cs | 1 + 5 files changed, 292 insertions(+), 164 deletions(-) create mode 100644 zzre.core/assetregistry/AssetHandleT.cs diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 1ed8fafa..99df7626 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using DotNext; using NUnit.Framework; using NUnit.Framework.Constraints; @@ -1184,6 +1185,124 @@ public void Handle_Default_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() { diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index df2fa568..9ba1ddb1 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -1,105 +1,8 @@ using System; -using System.Threading; -using System.Threading.Tasks; -using DotNext; - +using System.Diagnostics; namespace zzre; -/*public record struct AssetHandle(IAssetRegistry Registry, Guid AssetId) : IDisposable -{ - private bool wasDisposed; - private IDisposable? _asset; - private IDisposable? AssetOpt - { - get => Volatile.Read(ref _asset); - set => Interlocked.Exchange(ref _asset, value); - } - - internal AssetHandle(IAssetRegistry registry, Guid assetId, bool wasDisposed, IDisposable? asset) : this(registry, assetId) - { - this.wasDisposed = true; - this.AssetOpt = asset; - } - - public void Dispose() - { - if (wasDisposed) return; - wasDisposed = true; - AssetOpt = null; - ((IAssetRegistryInternal)Registry).DelRef(AssetId); - } - - public IDisposable? Asset - { - get - { - ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); - if (AssetOpt is null) - { - var result = ((IAssetRegistryInternal)Registry).GetAsset(AssetId).Value; - if (result?.IsSuccessful is true) - AssetOpt = result.Value.Value; - } - return AssetOpt; - } - } - - public IDisposable Get() - { - Registry.ThrowIfNotMainThread(); - if (AssetOpt is not null) - return AssetOpt; - var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); - if (!lazy.IsValueCreated) - lazy.WithCancellation(Registry.Cancellation).Wait(Registry.Cancellation); - return AssetOpt = lazy.Value!.Value.Value; // throws on error - } - - public ValueTask GetAsync(CancellationToken ct) - { - if (AssetOpt is IDisposable prevAsset) - return ValueTask.FromResult(prevAsset); - return new(DoGetAsync(ct)); - } - - private async Task DoGetAsync(CancellationToken ct) - { - var lazy = ((IAssetRegistryInternal)Registry).GetAsset(AssetId); - return AssetOpt = await lazy.WithCancellation(ct); - } - - public AssetHandle As() where TAsset : class, IDisposable - { - var result = new AssetHandle(Registry, AssetId, wasDisposed, (TAsset?)AssetOpt); - wasDisposed = true; - AssetOpt = null; - return result; - } - - public AssetHandle AsDuplicate() where TAsset : class, IDisposable - { - ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); - ((IAssetRegistryInternal)Registry).AddRef(AssetId); - return new(Registry, AssetId, false, AssetOpt); - } - - public AssetHandle Duplicate() - { - ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); - ((IAssetRegistryInternal)Registry).AddRef(AssetId); - return new(Registry, AssetId, false, AssetOpt); - } -}*/ - - -public interface IAssetHandle : IDisposable -{ - IAssetRegistry Registry { get; } - Guid AssetId { get; } -} - -public struct AssetHandle(IAssetRegistry registry, Guid assetId) : IAssetHandle, IEquatable> - where TAsset : class, IDisposable +public struct AssetHandle(IAssetRegistry registry, Guid assetId) : IAssetHandle, IEquatable { private bool wasDisposed; public readonly IAssetRegistry Registry => registry; @@ -118,79 +21,32 @@ public void Dispose() ((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() + public AssetHandle As() + where TAsset : class, IAsset { - 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() - { - var result = new AssetHandle(Registry, AssetId, wasDisposed, AssetOpt); + TypeCheck(typeof(TAsset)); + AssetHandle result = new(registry, assetId, wasDisposed); wasDisposed = true; - AssetOpt = null; return result; } - public AssetHandle AsDuplicate() + public readonly AssetHandle AsDuplicate() + where TAsset : class, IAsset { - ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ThrowIfDisposed(); + TypeCheck(typeof(TAsset)); ((IAssetRegistryInternal)Registry).AddRef(AssetId); - return new(Registry, AssetId, false, AssetOpt); - }*/ + return new(Registry, AssetId, false); + } - public AssetHandle Move() + public AssetHandle Move() { var result = this; wasDisposed = true; return result; } - public readonly AssetHandle Duplicate() + public readonly AssetHandle Duplicate() { ThrowIfDisposed(); ((IAssetRegistryInternal)Registry).AddRef(AssetId); @@ -199,19 +55,27 @@ public readonly AssetHandle Duplicate() private readonly void ThrowIfDisposed() { - ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); - ObjectDisposedException.ThrowIf(AssetId == Guid.Empty, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(wasDisposed, typeof(AssetHandle)); + ObjectDisposedException.ThrowIf(AssetId == Guid.Empty, typeof(AssetHandle)); ObjectDisposedException.ThrowIf(Registry?.WasDisposed is null or true, typeof(IAssetRegistry)); } + [Conditional("DEBUG")] + private readonly void TypeCheck(Type type) + { + if (wasDisposed || Registry?.WasDisposed is null or true || AssetId == Guid.Empty) + return; + ((IAssetRegistryInternal)Registry).CheckType(AssetId, type); + } + 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) => + 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) => + public static bool operator ==(AssetHandle a, AssetHandle b) => a.Equals(b); - public static bool operator !=(AssetHandle a, AssetHandle b) => + public static bool operator !=(AssetHandle a, AssetHandle b) => !a.Equals(b); } 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/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 3a6c6df2..6da4bac0 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -194,6 +194,21 @@ AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) return asset.LoadLazy; } + void IAssetRegistryInternal.CheckType(Guid assetId, Type type) + { + LockSemaphore(); + try + { + 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}"); + } + finally + { + semaphore.Release(); + } + } + public AssetHandle Load(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable where TAsset : class, IAsset diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 2e574910..2aebc49b 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -38,4 +38,5 @@ internal interface IAssetRegistryInternal : IAssetRegistry void AddRef(Guid assetId); void DelRef(Guid assetId); AsyncLazy GetAsset(Guid assetId); + void CheckType(Guid assetId, Type type); } From 0b4756cfe8b82ec406479233dcf45a4e4d926522 Mon Sep 17 00:00:00 2001 From: Helco Date: Mon, 28 Jul 2025 14:44:20 +0200 Subject: [PATCH 39/64] Add IAssetRegistry.CopyDebugInfo to fix AssetExplorer --- zzre.core/assetregistry/AssetRegistry.cs | 42 ++++++++++++++++++++--- zzre.core/assetregistry/IAssetRegistry.cs | 20 +++++++++++ zzre/tools/AssetExplorer.cs | 14 ++++---- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 6da4bac0..2499cca8 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -27,6 +27,8 @@ public IDisposable? Asset } } public int RefCount = 1; + public AssetPriority Priority; + public string? Name; } public class AssetRegistry : IAssetRegistryInternal @@ -219,7 +221,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio if (TAsset.Locality is AssetLocality.Global && IsLocalRegistry) return ParentRegistry!.Load(info, priority); - var (assetId, assetState) = GetOrCreateAssetState(info); + var (assetId, assetState) = GetOrCreateAssetState(info, priority); var handle = new AssetHandle(this, assetId); if (!assetState.LoadLazy.IsValueCreated) { @@ -249,7 +251,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio return handle; } - private (Guid, AssetState) GetOrCreateAssetState(in TInfo info) + private (Guid, AssetState) GetOrCreateAssetState(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable where TAsset : class, IAsset { @@ -273,6 +275,8 @@ public AssetHandle Load(in TInfo info, AssetPriority prio { SanityCheckSharedAsset(typeof(TAsset), assetState); assetState.RefCount++; + if (assetState.Asset is null && (int)priority < (int)assetState.Priority) + assetState.Priority = priority; return (assetId, assetState); } @@ -283,7 +287,8 @@ public AssetHandle Load(in TInfo info, AssetPriority prio NeedsMainThreadDisposal = TAsset.NeedsMainThreadDisposal, LoadLazy = new(ct => LoadAsset(infoCopy, assetId)), Tag = unchecked(++nextAssetTag), - AssetType = typeof(TAsset) + AssetType = typeof(TAsset), + Priority = priority }; assets[assetId] = assetState; return (assetId, assetState); @@ -348,7 +353,7 @@ void CheckRegistryDisposal() public bool TryGet(Guid assetId, out AssetHandle handle) where TAsset : class, IAsset { - ObjectDisposedException.ThrowIf(WasDisposed, typeof(AssetRegistry)); + ObjectDisposedException.ThrowIf(WasDisposed, typeof(AssetRegistry)); if (ParentRegistry?.TryGet(assetId, out handle) is true) return true; @@ -521,4 +526,33 @@ public void Apply(AssetHandle handle, Action } } } + + public void CopyDebugInfo(List infos) + { + LockSemaphore(); + try + { + 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 + )); + } + } + finally + { + semaphore.Release(); + } + } } diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index 2aebc49b..ce9ed424 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using DotNext.Threading; @@ -31,6 +32,25 @@ 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 diff --git a/zzre/tools/AssetExplorer.cs b/zzre/tools/AssetExplorer.cs index 962f8971..96293d43 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; } } @@ -86,7 +86,7 @@ private enum Column Priority } - private void HandleContentFor(IAssetRegistryDebug registry) + private void HandleContentFor(IAssetRegistry registry) { registry.CopyDebugInfo(assets); if (!BeginTable("Assets", 6, @@ -122,7 +122,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 +154,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 }; From 20e960e3469545011d2dfa0022ad14b003e2e66c Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 29 Jul 2025 08:27:16 +0200 Subject: [PATCH 40/64] Readd AssetRegistryStats --- zzre.core/assetregistry/AssetRegistry.cs | 11 ++- zzre.core/assetregistry/AssetRegistryStats.cs | 69 +++++++++++++++++++ zzre.core/assetregistry/IAssetRegistry.cs | 1 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 zzre.core/assetregistry/AssetRegistryStats.cs diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 2499cca8..3682039f 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -60,6 +60,7 @@ public class AssetRegistry : IAssetRegistryInternal public IAssetRegistry? ParentRegistry { get; } public CancellationToken Cancellation => cancellationSource.Token; public ITagContainer DIContainer { get; } + public AssetRegistryStats Stats { get; private set; } public AssetRegistry(ITagContainer diContainer, IAssetRegistry? parent = null, string? debugName = null) { @@ -101,7 +102,10 @@ public void Dispose() private void DisposeAssetState(AssetState state) { if (IsMainThread || !state.NeedsMainThreadDisposal) + { + Stats.OnAssetRemoved(); state.Asset?.Dispose(); + } else if (state.Asset is not null) { var success = assetsToDispose.Writer.TryWrite(state.Asset); @@ -291,6 +295,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio Priority = priority }; assets[assetId] = assetState; + Stats.OnAssetCreated(); return (assetId, assetState); } finally @@ -313,7 +318,7 @@ private async Task LoadAsset(TInfo info, Guid assetI { // Due to AsyncLazy we can flow exceptions outside this method - // Load asset and secondary assets + // Load asset var asset = (await TAsset.LoadAsync(this, assetId, info, Cancellation)).Asset; Debug.Assert(asset.Registry == this); CheckRegistryDisposal(); @@ -324,6 +329,7 @@ private async Task LoadAsset(TInfo info, Guid assetI { var assetState = assets.GetValueOrDefault(assetId); ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); + Stats.OnAssetLoaded(); } catch { @@ -467,7 +473,10 @@ private void DisposeOldAssets() Debug.Assert(IsMainThread); Debug.Assert(semaphore.CurrentCount == 0); while (assetsToDispose.Reader.TryRead(out var asset)) + { asset.Dispose(); + Stats.OnAssetRemoved(); + } } public void Apply(AssetHandle handle, Action> action) diff --git a/zzre.core/assetregistry/AssetRegistryStats.cs b/zzre.core/assetregistry/AssetRegistryStats.cs new file mode 100644 index 00000000..804b9fd4 --- /dev/null +++ b/zzre.core/assetregistry/AssetRegistryStats.cs @@ -0,0 +1,69 @@ +using System.Text; +using System.Threading; + +namespace zzre; + +/// +/// A snapshot of mostly monotonous counters of an +/// +public struct AssetRegistryStats +{ + private int created; + private int loaded; + private int removed; + private int total; + + /// The number of assets created + public readonly int Created => created; + /// The number of assets that finished loading + public readonly int Loaded => loaded; + /// The number of assets removed from the registry + public readonly int Removed => removed; + /// The number of currently registered assets + /// This counter is not monotonous + public readonly int Total => total; + + internal void OnAssetCreated() + { + Interlocked.Increment(ref created); + Interlocked.Increment(ref total); + } + + internal void OnAssetLoaded() => Interlocked.Increment(ref loaded); + + internal void OnAssetRemoved() + { + Interlocked.Increment(ref removed); + Interlocked.Decrement(ref total); + } + + public static AssetRegistryStats operator -(AssetRegistryStats lhs, AssetRegistryStats rhs) => new() + { + created = lhs.created - rhs.created, + loaded = lhs.loaded - rhs.loaded, + removed = lhs.removed - rhs.removed, + total = lhs.total - rhs.total, + }; + + public static AssetRegistryStats operator +(AssetRegistryStats lhs, AssetRegistryStats rhs) => new() + { + created = rhs.created + lhs.created, + loaded = rhs.loaded + lhs.loaded, + removed = rhs.removed + lhs.removed, + total = rhs.total + lhs.total, + }; + + public override readonly string ToString() + { + var builder = new StringBuilder(256); + builder.Append("Created: "); + builder.Append(created); + builder.Append(" Loaded: "); + builder.Append(loaded); + builder.Append(" Removed: "); + builder.Append(removed); + builder.Append(" Total: "); + builder.Append(total); + return builder.ToString(); + } +} diff --git a/zzre.core/assetregistry/IAssetRegistry.cs b/zzre.core/assetregistry/IAssetRegistry.cs index ce9ed424..83c30918 100644 --- a/zzre.core/assetregistry/IAssetRegistry.cs +++ b/zzre.core/assetregistry/IAssetRegistry.cs @@ -20,6 +20,7 @@ public interface IAssetRegistry : IDisposable IAssetRegistry? ParentRegistry { get; } bool IsLocalRegistry { get; } CancellationToken Cancellation { get; } // is triggered when registry is disposed + AssetRegistryStats Stats { get; } AssetHandle Load(in TInfo info, AssetPriority priority) where TInfo : struct, IEquatable From 6d38fc9d9f860f20f8527fce252ec09dad8a2bfc Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 29 Jul 2025 09:23:43 +0200 Subject: [PATCH 41/64] Add AssetRegistryDelayed --- zzre.core.tests/TestAssetRegistry.cs | 94 +++++++++++++++++++ zzre.core/assetregistry/AssetRegistry.cs | 11 ++- .../assetregistry/AssetRegistryDelayed.cs | 81 ++++++++++++++++ zzre.core/assetregistry/AssetRegistryStats.cs | 10 +- 4 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 zzre.core/assetregistry/AssetRegistryDelayed.cs diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 99df7626..c2889df8 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1760,4 +1760,98 @@ public void TryGet_LocalFromGlobal() 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 + ))); + } + + [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.DelayDeletion = 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.DelayDeletion = false; + Assert.That(handle2.Get, Throws.Nothing); + } + + [Test] + public void Delayed_DelayedTwice() + { + using var global = new AssetRegistryDelayed(new AssetRegistry(DI)); + global.DelayDeletion = 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.DelayDeletion = false; + Assert.That(handle2.Get, Throws.Nothing); + + global.DelayDeletion = true; + global.DelayDeletion = false; + Assert.That(handle2.Get, Throws.Nothing); // still alive + } } diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 3682039f..57dbb761 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -53,6 +53,7 @@ public class AssetRegistry : IAssetRegistryInternal private readonly Dictionary> applyActionCaster = []; private List<(Guid assetId, uint tag, Type assetType, object action)> applyActions = [], applyActionsBackup = []; private uint nextAssetTag; + private AssetRegistryStats localStats; public bool WasDisposed => cancellationSource.IsCancellationRequested; public bool IsMainThread => mainThreadId == Environment.CurrentManagedThreadId; @@ -60,7 +61,7 @@ public class AssetRegistry : IAssetRegistryInternal public IAssetRegistry? ParentRegistry { get; } public CancellationToken Cancellation => cancellationSource.Token; public ITagContainer DIContainer { get; } - public AssetRegistryStats Stats { get; private set; } + public AssetRegistryStats Stats => (ParentRegistry?.Stats ?? default) + localStats; public AssetRegistry(ITagContainer diContainer, IAssetRegistry? parent = null, string? debugName = null) { @@ -103,7 +104,7 @@ private void DisposeAssetState(AssetState state) { if (IsMainThread || !state.NeedsMainThreadDisposal) { - Stats.OnAssetRemoved(); + localStats.OnAssetRemoved(); state.Asset?.Dispose(); } else if (state.Asset is not null) @@ -295,7 +296,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio Priority = priority }; assets[assetId] = assetState; - Stats.OnAssetCreated(); + localStats.OnAssetCreated(); return (assetId, assetState); } finally @@ -329,7 +330,7 @@ private async Task LoadAsset(TInfo info, Guid assetI { var assetState = assets.GetValueOrDefault(assetId); ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); - Stats.OnAssetLoaded(); + localStats.OnAssetLoaded(); } catch { @@ -475,7 +476,7 @@ private void DisposeOldAssets() while (assetsToDispose.Reader.TryRead(out var asset)) { asset.Dispose(); - Stats.OnAssetRemoved(); + localStats.OnAssetRemoved(); } } diff --git a/zzre.core/assetregistry/AssetRegistryDelayed.cs b/zzre.core/assetregistry/AssetRegistryDelayed.cs new file mode 100644 index 00000000..4aa1d53c --- /dev/null +++ b/zzre.core/assetregistry/AssetRegistryDelayed.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using DotNext.Threading; + +namespace zzre; + +public sealed class AssetRegistryDelayed(IAssetRegistry Inner) : IAssetRegistryInternal +{ + public bool WasDisposed => Inner.WasDisposed; + public bool IsMainThread => Inner.IsMainThread; + public ITagContainer DIContainer => Inner.DIContainer; + public IAssetRegistry? ParentRegistry => Inner.ParentRegistry; + public bool IsLocalRegistry => Inner.IsLocalRegistry; + public CancellationToken Cancellation => Inner.Cancellation; + public AssetRegistryStats Stats => Inner.Stats; + + public void CopyDebugInfo(List assetInfos) => + Inner.CopyDebugInfo(assetInfos); + + public void Dispose() => Inner.Dispose(); + public void Update() => Inner.Update(); + + void IAssetRegistryInternal.AddRef(Guid assetId) => + ((IAssetRegistryInternal)Inner).AddRef(assetId); + + public void Apply(AssetHandle handle, Action> action) + where TAsset : class, IAsset => + Inner.Apply(handle, action); + + void IAssetRegistryInternal.CheckType(Guid assetId, Type type) => + ((IAssetRegistryInternal)Inner).CheckType(assetId, type); + + AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) => + ((IAssetRegistryInternal)Inner).GetAsset(assetId); + + public bool TryGet(Guid assetId, out AssetHandle handle) + where TAsset : class, IAsset => + Inner.TryGet(assetId, out handle); + + public bool DelayDeletion + { + 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 (DelayDeletion) + { + 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 804b9fd4..e27fab87 100644 --- a/zzre.core/assetregistry/AssetRegistryStats.cs +++ b/zzre.core/assetregistry/AssetRegistryStats.cs @@ -6,12 +6,12 @@ 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 readonly int Created => created; From ce410e644e9de5fe2475f0388fd3aaa22612cb7e Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 29 Jul 2025 09:28:37 +0200 Subject: [PATCH 42/64] Improve code coverage --- zzre.core.tests/TestAssetRegistry.cs | 12 ++++++++++++ zzre.core/assetregistry/AssetRegistryDelayed.cs | 16 ++++++++++++++++ zzre.core/assetregistry/IAsset.cs | 2 ++ 3 files changed, 30 insertions(+) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index c2889df8..37070d86 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1805,6 +1805,18 @@ public void Stats() 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] diff --git a/zzre.core/assetregistry/AssetRegistryDelayed.cs b/zzre.core/assetregistry/AssetRegistryDelayed.cs index 4aa1d53c..a9551fd4 100644 --- a/zzre.core/assetregistry/AssetRegistryDelayed.cs +++ b/zzre.core/assetregistry/AssetRegistryDelayed.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading; using DotNext.Threading; @@ -8,33 +9,48 @@ 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); diff --git a/zzre.core/assetregistry/IAsset.cs b/zzre.core/assetregistry/IAsset.cs index b8e31c4a..6138c445 100644 --- a/zzre.core/assetregistry/IAsset.cs +++ b/zzre.core/assetregistry/IAsset.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -14,6 +15,7 @@ public enum AssetLocality 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; } From 209c2ac92f391ddc156601db68fec836895f1c41 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 31 Jul 2025 09:57:41 +0200 Subject: [PATCH 43/64] Shallow fixes using AssetRegistryDelayed --- zzre.core.tests/TestAssetRegistry.cs | 12 ++++++------ zzre.core/assetregistry/AssetHandle.cs | 8 ++++++++ zzre.core/assetregistry/AssetRegistryDelayed.cs | 4 ++-- zzre/Program.cs | 17 +---------------- zzre/game/Game.cs | 9 +++++---- zzre/game/UI.cs | 9 +++++---- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 37070d86..74802d62 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1835,7 +1835,7 @@ public void Delayed_Undelayed() public void Delayed_Delayed() { using var global = new AssetRegistryDelayed(new AssetRegistry(DI)); - global.DelayDeletion = true; + 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 @@ -1843,7 +1843,7 @@ public void Delayed_Delayed() Assert.That(handle2.Get, Throws.Nothing); - global.DelayDeletion = false; + global.DelayDisposals = false; Assert.That(handle2.Get, Throws.Nothing); } @@ -1851,7 +1851,7 @@ public void Delayed_Delayed() public void Delayed_DelayedTwice() { using var global = new AssetRegistryDelayed(new AssetRegistry(DI)); - global.DelayDeletion = true; + global.DelayDisposals = true; var handle1 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); var handle2 = global.Load(GetInfo(1).AsCompleted(), AssetPriority.Synchronous); @@ -1859,11 +1859,11 @@ public void Delayed_DelayedTwice() Assert.That(handle2.Get, Throws.Nothing); - global.DelayDeletion = false; + global.DelayDisposals = false; Assert.That(handle2.Get, Throws.Nothing); - global.DelayDeletion = true; - global.DelayDeletion = false; + global.DelayDisposals = true; + global.DelayDisposals = false; Assert.That(handle2.Get, Throws.Nothing); // still alive } } diff --git a/zzre.core/assetregistry/AssetHandle.cs b/zzre.core/assetregistry/AssetHandle.cs index 9ba1ddb1..404ac7be 100644 --- a/zzre.core/assetregistry/AssetHandle.cs +++ b/zzre.core/assetregistry/AssetHandle.cs @@ -21,6 +21,14 @@ public void Dispose() ((IAssetRegistryInternal)Registry).DelRef(AssetId); } + public readonly TAsset Get() + where TAsset : class, IAsset + { + TypeCheck(typeof(TAsset)); + var tmp = new AssetHandle(registry, assetId, wasDisposed); // an invalid copy + return tmp.Get(); + } + public AssetHandle As() where TAsset : class, IAsset { diff --git a/zzre.core/assetregistry/AssetRegistryDelayed.cs b/zzre.core/assetregistry/AssetRegistryDelayed.cs index a9551fd4..c84e65ea 100644 --- a/zzre.core/assetregistry/AssetRegistryDelayed.cs +++ b/zzre.core/assetregistry/AssetRegistryDelayed.cs @@ -55,7 +55,7 @@ public bool TryGet(Guid assetId, out AssetHandle handle) where TAsset : class, IAsset => Inner.TryGet(assetId, out handle); - public bool DelayDeletion + public bool DelayDisposals { get => Volatile.Read(ref delayDeletion); set @@ -86,7 +86,7 @@ public AssetHandle Load(in TInfo info, AssetPriority prio void IAssetRegistryInternal.DelRef(Guid assetId) { - if (DelayDeletion) + if (DelayDisposals) { lock (assetIdsToDelete) assetIdsToDelete.Add(assetId); diff --git a/zzre/Program.cs b/zzre/Program.cs index 89a4833d..ac6c8ad7 100644 --- a/zzre/Program.cs +++ b/zzre/Program.cs @@ -146,23 +146,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/game/Game.cs b/zzre/game/Game.cs index e938cea1..18908745 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 @@ -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); diff --git a/zzre/game/UI.cs b/zzre/game/UI.cs index 533cefa1..092a4d68 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,7 +40,7 @@ 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)); @@ -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); } From 1a43dbf39c24fb81acff159ee5441fbb0c78e91e Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 31 Jul 2025 16:05:18 +0200 Subject: [PATCH 44/64] Shallow fixes with delay, stats, SoundEmitter --- zzre/Program.InDev.cs | 4 ++-- zzre/game/components/sound/SoundBuffer.cs | 3 --- zzre/game/systems/sound/SoundEmitter.cs | 23 +++++++++++++---------- zzre/tools/ActorEditor.Part.cs | 5 ++--- zzre/tools/AssetExplorer.cs | 3 ++- 5 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 zzre/game/components/sound/SoundBuffer.cs diff --git a/zzre/Program.InDev.cs b/zzre/Program.InDev.cs index 1c1a576c..b0f1154f 100644 --- a/zzre/Program.InDev.cs +++ b/zzre/Program.InDev.cs @@ -151,7 +151,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 +161,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/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/systems/sound/SoundEmitter.cs b/zzre/game/systems/sound/SoundEmitter.cs index a4560900..ae6106e6 100644 --- a/zzre/game/systems/sound/SoundEmitter.cs +++ b/zzre/game/systems/sound/SoundEmitter.cs @@ -80,32 +80,35 @@ 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_)); + } } - 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/tools/ActorEditor.Part.cs b/zzre/tools/ActorEditor.Part.cs index 1c4c0d43..12f51f42 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), AssetPriority.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(); diff --git a/zzre/tools/AssetExplorer.cs b/zzre/tools/AssetExplorer.cs index 96293d43..e9237e71 100644 --- a/zzre/tools/AssetExplorer.cs +++ b/zzre/tools/AssetExplorer.cs @@ -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(); From a315f78df3dedbb6731c9dbdcf6d8ea357ebafae Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 31 Jul 2025 16:22:20 +0200 Subject: [PATCH 45/64] Fix LensFlare, SoundEmitter, ActorEditor.Part --- zzre/game/systems/effect/LensFlare.cs | 4 +++- zzre/game/systems/sound/SoundEmitter.cs | 3 ++- zzre/tools/ActorEditor.Part.cs | 7 +++---- 3 files changed, 8 insertions(+), 6 deletions(-) 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/sound/SoundEmitter.cs b/zzre/game/systems/sound/SoundEmitter.cs index ae6106e6..3653d78f 100644 --- a/zzre/game/systems/sound/SoundEmitter.cs +++ b/zzre/game/systems/sound/SoundEmitter.cs @@ -80,7 +80,7 @@ private unsafe void HandleSpawnSample(in messages.SpawnSample msg) Parent = msg.ParentLocation }); } - + var handle = assetRegistry.LoadSound(new zzio.FilePath(msg.SamplePath), msg.Priority); if (msg.Priority is AssetPriority.Synchronous) ApplySpawnSample(handle, entity, msg); @@ -89,6 +89,7 @@ private unsafe void HandleSpawnSample(in messages.SpawnSample msg) var msg_ = msg; assetRegistry.Apply(handle, h => ApplySpawnSample(h, entity, msg_)); } + entity.Set(handle.As()); } private void ApplySpawnSample(AssetHandle handle, DefaultEcs.Entity entity, messages.SpawnSample msg) diff --git a/zzre/tools/ActorEditor.Part.cs b/zzre/tools/ActorEditor.Part.cs index 12f51f42..1b021acb 100644 --- a/zzre/tools/ActorEditor.Part.cs +++ b/zzre/tools/ActorEditor.Part.cs @@ -67,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, - AssetPriority.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); @@ -93,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(); } From f9e659eede6cc1708ec0732767e40331d8792d95 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 31 Jul 2025 16:30:45 +0200 Subject: [PATCH 46/64] Fix BeamStar, EffectCombiner, MovingPlanes, ParticleEmitter, RandomPlanes --- zzre/game/systems/actor/ActorRenderer.cs | 8 +++----- zzre/game/systems/effect/BeamStar.cs | 4 +++- zzre/game/systems/effect/EffectCombiner.cs | 6 ++++-- zzre/game/systems/effect/MovingPlanes.cs | 4 +++- zzre/game/systems/effect/ParticleEmitter.cs | 4 +++- zzre/game/systems/effect/RandomPlanes.cs | 4 +++- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/zzre/game/systems/actor/ActorRenderer.cs b/zzre/game/systems/actor/ActorRenderer.cs index 77d7a45f..594ed167 100644 --- a/zzre/game/systems/actor/ActorRenderer.cs +++ b/zzre/game/systems/actor/ActorRenderer.cs @@ -169,25 +169,23 @@ ref readonly (ActorRenderer thiz, DefaultEcs.Entity entity) context) private DefaultEcs.Entity CreateActorPart( DefaultEcs.Entity parent, - AssetHandle clumpHandle, - IReadOnlyList> animations, + ClumpAsset clumpAsset, + IReadOnlyList 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); + animationPool.Add(partDescr.animations[i].type, animations[i].Animation); LoadActorPartMaterials(part, clumpAsset.Mesh); 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 8159a593..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, AssetPriority.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/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)); } From 3051936645e8ad51bc276ad6955f9a8c7bc8e836 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 08:16:38 +0200 Subject: [PATCH 47/64] Adapter LoadUIBitmapFor and LoadUITileSheetFor --- zzre/game/EntityAssetExtensions.cs | 39 ++++++++++++++++++++++++++++++ zzre/game/systems/ui/Label.cs | 2 +- zzre/game/systems/ui/ScrDeck.cs | 2 +- zzre/game/systems/ui/ScrMapMenu.cs | 8 +++--- zzre/game/uibuilder/ButtonLike.cs | 2 +- zzre/game/uibuilder/Image.cs | 7 +++--- zzre/game/uibuilder/LabelLike.cs | 3 +-- 7 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 zzre/game/EntityAssetExtensions.cs diff --git a/zzre/game/EntityAssetExtensions.cs b/zzre/game/EntityAssetExtensions.cs new file mode 100644 index 00000000..afc29597 --- /dev/null +++ b/zzre/game/EntityAssetExtensions.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using zzre.rendering; + +namespace zzre.game; + +public static class EntityAssetExtensions +{ + 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; + } +} 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; From 1f9f993e3eb87d0cfd01818579416f52f4d0a773 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 10:40:35 +0200 Subject: [PATCH 48/64] Fix rest of asset loading --- zzre/assets/ClumpMaterialAsset.cs | 18 +++++++ zzre/game/EntityAssetExtensions.cs | 60 ++++++++++++++++++++- zzre/game/systems/actor/ActorRenderer.cs | 64 +++++++++++------------ zzre/game/systems/effect/ModelEmitter.cs | 6 +-- zzre/game/systems/model/BackdropLoader.cs | 15 +++--- zzre/game/systems/model/ModelLoader.cs | 28 +++++----- zzre/game/uibuilder/UIBuilder.cs | 2 +- 7 files changed, 133 insertions(+), 60 deletions(-) diff --git a/zzre/assets/ClumpMaterialAsset.cs b/zzre/assets/ClumpMaterialAsset.cs index eca520ea..3905a633 100644 --- a/zzre/assets/ClumpMaterialAsset.cs +++ b/zzre/assets/ClumpMaterialAsset.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Veldrid; using zzio; +using zzio.rwbs; using zzre.materials; using zzre.rendering; using static zzre.ClumpMaterialAsset; @@ -94,4 +95,21 @@ public static AssetHandle LoadClumpMaterial(this IAssetRegis new(textureName, sampler, config, texturePlaceholder), priority ); + + public static AssetHandle LoadClumpMaterial(this IAssetRegistry registry, + RWMaterial rwMaterial, + 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.LoadClumpMaterial( + rwTextureName, + samplerDescription, + config, + texturePlaceholder, + priority); + } } diff --git a/zzre/game/EntityAssetExtensions.cs b/zzre/game/EntityAssetExtensions.cs index afc29597..248dcae0 100644 --- a/zzre/game/EntityAssetExtensions.cs +++ b/zzre/game/EntityAssetExtensions.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Numerics; +using zzre.materials; using zzre.rendering; namespace zzre.game; @@ -25,7 +27,7 @@ public static TileSheet LoadUITileSheetFor(this IAssetRegistry registry, Default entity.Set(asset.TileSheet); return asset.TileSheet; } - + public static TileSheet LoadUITileSheetFor(this IAssetRegistry registry, ref DefaultEcs.Command.EntityRecord entity, in UITileSheetAsset.Info info) { @@ -34,6 +36,60 @@ public static TileSheet LoadUITileSheetFor(this IAssetRegistry registry, ref Def entity.Set(handle.As()); entity.Set(asset.Material); entity.Set(asset.TileSheet); - return 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/systems/actor/ActorRenderer.cs b/zzre/game/systems/actor/ActorRenderer.cs index 594ed167..12d23485 100644 --- a/zzre/game/systems/actor/ActorRenderer.cs +++ b/zzre/game/systems/actor/ActorRenderer.cs @@ -130,47 +130,43 @@ 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, ClumpAsset clumpAsset, - IReadOnlyList animations, + ReadOnlySpan animations, ActorPartDescription partDescr) { var part = parent.World.CreateEntity(); @@ -184,7 +180,7 @@ private DefaultEcs.Entity CreateActorPart( part.Set(new Skeleton(clumpAsset.Mesh.Skin, clumpAsset.Name)); ref var animationPool = ref part.Get(); - for (int i = 0; i < animations.Count; i++) + for (int i = 0; i < animations.Length; i++) animationPool.Add(partDescr.animations[i].type, animations[i].Animation); LoadActorPartMaterials(part, clumpAsset.Mesh); @@ -200,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/effect/ModelEmitter.cs b/zzre/game/systems/effect/ModelEmitter.cs index 220fb64d..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, - AssetPriority.Low, new(data.renderMode, playback.DepthTest), - StandardTextureKind.Clear); + StandardTextureKind.Clear, + AssetPriority.Low); entity.Set(); } diff --git a/zzre/game/systems/model/BackdropLoader.cs b/zzre/game/systems/model/BackdropLoader.cs index 9b8a340a..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, AssetPriority.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 5612cb30..cd22ee30 100644 --- a/zzre/game/systems/model/ModelLoader.cs +++ b/zzre/game/systems/model/ModelLoader.cs @@ -133,7 +133,7 @@ private void HandleLoadModel(in messages.LoadModel msg) => 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) diff --git a/zzre/game/uibuilder/UIBuilder.cs b/zzre/game/uibuilder/UIBuilder.cs index 475efea8..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(), AssetPriority.High).As(); + preloadAssetHandle = assetRegistry.Load(new(), AssetPriority.High); } protected override void DisposeManaged() From 3ae980f1a38088f10359675aedf05679d25ab033 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 10:45:47 +0200 Subject: [PATCH 49/64] Reintegrate ECS with asset registry --- zzre/game/EntityAssetExtensions.cs | 15 +++++++++++++++ zzre/game/Game.cs | 2 +- zzre/game/UI.cs | 2 +- zzre/tools/WorldViewer.cs | 3 ++- zzre/tools/effecteditor/EffectEditor.cs | 2 +- zzre/tools/sceneeditor/SceneEditor.cs | 2 +- 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/zzre/game/EntityAssetExtensions.cs b/zzre/game/EntityAssetExtensions.cs index 248dcae0..f5e0b215 100644 --- a/zzre/game/EntityAssetExtensions.cs +++ b/zzre/game/EntityAssetExtensions.cs @@ -7,6 +7,21 @@ 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) { diff --git a/zzre/game/Game.cs b/zzre/game/Game.cs index 18908745..57393484 100644 --- a/zzre/game/Game.cs +++ b/zzre/game/Game.cs @@ -67,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; } diff --git a/zzre/game/UI.cs b/zzre/game/UI.cs index 092a4d68..28e061a8 100644 --- a/zzre/game/UI.cs +++ b/zzre/game/UI.cs @@ -46,7 +46,7 @@ public UI(ITagContainer diContainer) 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 diff --git a/zzre/tools/WorldViewer.cs b/zzre/tools/WorldViewer.cs index 9a8f96af..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; @@ -156,7 +157,7 @@ public WorldViewer(ITagContainer diContainer) localDiContainer .AddTag(ecsWorld = new DefaultEcs.World()) .AddTag(assetRegistry = new AssetRegistry(localDiContainer, globalAssetRegistry, "WorldViewer")); - AssetRegistry.SubscribeAt(ecsWorld); + assetRegistry.SubscribeAt(ecsWorld); worldRenderer = new(localDiContainer); AddDisposable(worldRenderer); } diff --git a/zzre/tools/effecteditor/EffectEditor.cs b/zzre/tools/effecteditor/EffectEditor.cs index 2e5ab489..a0b6ccf6 100644 --- a/zzre/tools/effecteditor/EffectEditor.cs +++ b/zzre/tools/effecteditor/EffectEditor.cs @@ -87,7 +87,7 @@ public EffectEditor(ITagContainer parentDiContainer) diContainer, diContainer.GetTag(), "EffectEditor")); - AssetRegistry.SubscribeAt(ecsWorld); + game.EntityAssetExtensions.SubscribeAt(assetRegistry, ecsWorld); controls = new OrbitControlsTag(Window, camera.Location, diContainer); AddDisposable(controls); diff --git a/zzre/tools/sceneeditor/SceneEditor.cs b/zzre/tools/sceneeditor/SceneEditor.cs index a4289a40..44de5045 100644 --- a/zzre/tools/sceneeditor/SceneEditor.cs +++ b/zzre/tools/sceneeditor/SceneEditor.cs @@ -76,7 +76,7 @@ public SceneEditor(ITagContainer diContainer) .AddTag(assetRegistry = new AssetRegistry(localDiContainer, globalAssetRegistry, "SceneEditor")) .AddTag(ecsWorld = new DefaultEcs.World()) .AddTag(camera); - AssetRegistry.SubscribeAt(ecsWorld); + game.EntityAssetExtensions.SubscribeAt(assetRegistry, ecsWorld); new MiscComponent(localDiContainer); new DatasetComponent(localDiContainer); new WorldComponent(localDiContainer); From f8476480766047b9444455e055082e3b7d014218 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 11:32:40 +0200 Subject: [PATCH 50/64] Fix spurious NUnit exceptions due to not awaiting Assert.ThatAsync --- zzre.core.tests/TestAssetRegistry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 74802d62..77b9fde6 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -1614,7 +1614,7 @@ public async Task Apply_ErrorDuringLoad(CancellationToken ct) global.Apply(handle, _ => counter++); info.FinishLoad.SetException(new TestException()); - Assert.ThatAsync(async () => + await Assert.ThatAsync(async () => { await handle.GetAsync(ct); }, Throws.InstanceOf()); @@ -1631,7 +1631,7 @@ public async Task Apply_ErrorAfterLoad(CancellationToken ct) var info = GetInfo(1).AsErroneous(); using var handle = global.Load(info, AssetPriority.High); - Assert.ThatAsync(async () => + await Assert.ThatAsync(async () => { await handle.GetAsync(ct); }, Throws.InstanceOf()); @@ -1697,7 +1697,7 @@ public async Task TryGet_ErrorDuringLoad(CancellationToken ct) await info.StartedLoad.Task; Assert.That(global.TryGet(handle.AssetId, out var handle2), Is.True); - Assert.ThatAsync(() => handle2.GetAsync(ct).AsTask(), Throws.InstanceOf()); + await Assert.ThatAsync(() => handle2.GetAsync(ct).AsTask(), Throws.InstanceOf()); } [Test] From e22983b80e15e110da3a27c5fdf1556a24e0491e Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 17 Oct 2024 14:24:00 +0200 Subject: [PATCH 51/64] Add basic validator program --- .editorconfig | 3 + zzre/Program.InDev.cs | 13 +-- zzre/Program.Logging.cs | 8 +- zzre/Program.OpenAL.cs | 20 ++--- zzre/Program.Validation.cs | 74 +++++++++++++++++ zzre/Program.cs | 18 +++-- zzre/game/Game.cs | 1 + zzre/tools/validation/Validator.cs | 123 +++++++++++++++++++++++++++++ 8 files changed, 230 insertions(+), 30 deletions(-) create mode 100644 zzre/Program.Validation.cs create mode 100644 zzre/tools/validation/Validator.cs 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/zzre/Program.InDev.cs b/zzre/Program.InDev.cs index b0f1154f..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); 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.Validation.cs b/zzre/Program.Validation.cs new file mode 100644 index 00000000..9d87bb50 --- /dev/null +++ b/zzre/Program.Validation.cs @@ -0,0 +1,74 @@ +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); + 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(); + + 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 ac6c8ad7..d820777f 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); } diff --git a/zzre/game/Game.cs b/zzre/game/Game.cs index 57393484..42479f93 100644 --- a/zzre/game/Game.cs +++ b/zzre/game/Game.cs @@ -119,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/tools/validation/Validator.cs b/zzre/tools/validation/Validator.cs new file mode 100644 index 00000000..92151773 --- /dev/null +++ b/zzre/tools/validation/Validator.cs @@ -0,0 +1,123 @@ +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; + +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 uint queuedFileCount; + private uint processedFileCount; + private uint faultyFileCount; + + public uint QueuedFileCount => queuedFileCount; + public uint ProcessedFileCount => processedFileCount; + public uint FaultyFileCount => faultyFileCount; + + 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(); + logger.Information("Validation finished in {Elapsed}", stopwatch.Elapsed); + } + + private IEnumerable TraverseResourcePool(IResourcePool pool) + { + if (pool.Root.Type is ResourceType.File) + { + 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) + { + try + { + switch (Path.GetExtension(resource.Name).ToLowerInvariant()) + { + case ".bsp": await ValidateWorld(resource); break; + case ".scn": ValidateScene(resource); break; + default: + logger.Verbose("Ignored file (due to extension): {FileName}", resource.Name); + break; + } + } + catch (Exception e) + { + logger.Error("Exception when processing {Resource}: {Exception}", resource.Name, e); + Interlocked.Increment(ref faultyFileCount); + } + finally + { + Interlocked.Increment(ref processedFileCount); + } + } + + private async Task ValidateWorld(IResource resource) + { + using var handle = assetRegistry.LoadWorld(resource.Path, AssetLoadPriority.High); + await assetRegistry.WaitAsyncAll([ handle.Inner ]); + var world = handle.Get(); + var collider = WorldCollider.Create(world.Mesh.World); + } + + private 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); + } +} From d0da9c23ff560616677a42554fff731006c42e91 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 17 Oct 2024 16:48:41 +0200 Subject: [PATCH 52/64] validator: Add textures, models, actors and effects --- zzre/tools/validation/Validator.cs | 48 +++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/zzre/tools/validation/Validator.cs b/zzre/tools/validation/Validator.cs index 92151773..32e92abd 100644 --- a/zzre/tools/validation/Validator.cs +++ b/zzre/tools/validation/Validator.cs @@ -56,7 +56,7 @@ public async Task Run(CancellationToken ct) resourceTraversalBlock.Complete(); await processResourceBlock.Completion; stopwatch.Stop(); - logger.Information("Validation finished in {Elapsed}", stopwatch.Elapsed); + logger.Information("Validation of {ProcessedFileCount} resources finished in {Elapsed}", processedFileCount, stopwatch.Elapsed); } private IEnumerable TraverseResourcePool(IResourcePool pool) @@ -83,14 +83,21 @@ private IEnumerable TraverseResourcePool(IResourcePool pool) private async Task ValidateResource(IResource resource) { + bool wasIgnored = false; try { switch (Path.GetExtension(resource.Name).ToLowerInvariant()) { 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: logger.Verbose("Ignored file (due to extension): {FileName}", resource.Name); + wasIgnored = true; break; } } @@ -101,23 +108,56 @@ private async Task ValidateResource(IResource resource) } finally { - Interlocked.Increment(ref processedFileCount); + if (!wasIgnored) + Interlocked.Increment(ref processedFileCount); } } private async Task ValidateWorld(IResource resource) { using var handle = assetRegistry.LoadWorld(resource.Path, AssetLoadPriority.High); - await assetRegistry.WaitAsyncAll([ handle.Inner ]); + await assetRegistry.WaitAsyncAll([handle.Inner]); var world = handle.Get(); var collider = WorldCollider.Create(world.Mesh.World); } - private void ValidateScene(IResource resource) + private async Task ValidateClump(IResource resource) + { + using var handle = assetRegistry.Load( + new ClumpAsset.Info(resource.Path), + AssetLoadPriority.High).As(); + await assetRegistry.WaitAsyncAll([handle.Inner]); + var world = handle.Get(); + var collider = GeometryCollider.Create(world.Mesh.Geometry, location: null); + } + + private Task ValidateTexture(IResource resource) + { + using var handle = assetRegistry.LoadTexture(resource.Path, AssetLoadPriority.Synchronous); + return assetRegistry.WaitAsyncAll([handle.Inner]); + } + + 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 Task ValidateActor(IResource resource) + { + using var handle = assetRegistry.Load( + new ActorAsset.Info(Path.GetFileNameWithoutExtension(resource.Name)), + AssetLoadPriority.Synchronous); + return assetRegistry.WaitAsyncAll([handle]); + } + + private Task ValidateEffect(IResource resource) + { + using var handle = assetRegistry.Load( + new EffectCombinerAsset.Info(resource.Path), + AssetLoadPriority.Synchronous); + return assetRegistry.WaitAsyncAll([handle]); + } } From 827774b2061352fb95523292a21b20dd03358c44 Mon Sep 17 00:00:00 2001 From: Helco Date: Tue, 22 Oct 2024 08:38:41 +0200 Subject: [PATCH 53/64] Actually use MaxConcurrency option --- zzre/Program.Validation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zzre/Program.Validation.cs b/zzre/Program.Validation.cs index 9d87bb50..1fd9536b 100644 --- a/zzre/Program.Validation.cs +++ b/zzre/Program.Validation.cs @@ -30,7 +30,10 @@ private static void HandleValidation(InvocationContext ctx) CommonStartupAfterWindow(diContainer); using var cancellationSource = new CancellationTokenSource(); - var validator = new Validator(diContainer); + var validator = new Validator(diContainer) + { + MaxConcurrency = ctx.ParseResult.GetValueForOption(OptionValidationConcurrency) + }; Task.Run(async () => { WriteConsoleLine("Validation: starting..."); From 0b08fa0d405e4b6570a3250ba3db2d9713aa8e49 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 14:17:36 +0200 Subject: [PATCH 54/64] Adapt Validator --- zzre.core/math/WorldCollider.cs | 5 +++-- zzre/Program.cs | 2 +- zzre/tools/validation/Validator.cs | 31 +++++++++++++----------------- 3 files changed, 17 insertions(+), 21 deletions(-) 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/Program.cs b/zzre/Program.cs index d820777f..c18b10c3 100644 --- a/zzre/Program.cs +++ b/zzre/Program.cs @@ -123,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))]) }; } diff --git a/zzre/tools/validation/Validator.cs b/zzre/tools/validation/Validator.cs index 32e92abd..e9bf40d9 100644 --- a/zzre/tools/validation/Validator.cs +++ b/zzre/tools/validation/Validator.cs @@ -63,6 +63,7 @@ private IEnumerable TraverseResourcePool(IResourcePool pool) { if (pool.Root.Type is ResourceType.File) { + Interlocked.Increment(ref queuedFileCount); yield return pool.Root; yield break; } @@ -115,26 +116,22 @@ private async Task ValidateResource(IResource resource) private async Task ValidateWorld(IResource resource) { - using var handle = assetRegistry.LoadWorld(resource.Path, AssetLoadPriority.High); - await assetRegistry.WaitAsyncAll([handle.Inner]); - var world = handle.Get(); + 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 ClumpAsset.Info(resource.Path), - AssetLoadPriority.High).As(); - await assetRegistry.WaitAsyncAll([handle.Inner]); - var world = handle.Get(); + 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 Task ValidateTexture(IResource resource) { - using var handle = assetRegistry.LoadTexture(resource.Path, AssetLoadPriority.Synchronous); - return assetRegistry.WaitAsyncAll([handle.Inner]); + using var handle = assetRegistry.LoadTexture(resource.Path, AssetPriority.Synchronous); + return Task.CompletedTask; } private static void ValidateScene(IResource resource) @@ -147,17 +144,15 @@ private static void ValidateScene(IResource resource) private Task ValidateActor(IResource resource) { - using var handle = assetRegistry.Load( - new ActorAsset.Info(Path.GetFileNameWithoutExtension(resource.Name)), - AssetLoadPriority.Synchronous); - return assetRegistry.WaitAsyncAll([handle]); + using var handle = assetRegistry.LoadActor( + Path.GetFileNameWithoutExtension(resource.Name), + AssetPriority.Synchronous); + return Task.CompletedTask; } private Task ValidateEffect(IResource resource) { - using var handle = assetRegistry.Load( - new EffectCombinerAsset.Info(resource.Path), - AssetLoadPriority.Synchronous); - return assetRegistry.WaitAsyncAll([handle]); + using var handle = assetRegistry.LoadEffectCombiner(resource.Path, AssetPriority.Synchronous); + return Task.CompletedTask; } } From 1e2141a281cc2d047a56d063f2c0472d7cb5143e Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 1 Aug 2025 14:37:49 +0200 Subject: [PATCH 55/64] Fix synchronous validation tasks --- zzio.sln | 1 + zzre/tools/validation/Validator.cs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) 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/tools/validation/Validator.cs b/zzre/tools/validation/Validator.cs index e9bf40d9..77b21476 100644 --- a/zzre/tools/validation/Validator.cs +++ b/zzre/tools/validation/Validator.cs @@ -56,7 +56,7 @@ public async Task Run(CancellationToken ct) resourceTraversalBlock.Complete(); await processResourceBlock.Completion; stopwatch.Stop(); - logger.Information("Validation of {ProcessedFileCount} resources finished in {Elapsed}", processedFileCount, stopwatch.Elapsed); + logger.Information("Validation of {ProcessedFileCount} ({FaultyFileCount} faulty) resources finished in {Elapsed}", processedFileCount, faultyFileCount, stopwatch.Elapsed); } private IEnumerable TraverseResourcePool(IResourcePool pool) @@ -128,10 +128,10 @@ private async Task ValidateClump(IResource resource) var collider = GeometryCollider.Create(world.Mesh.Geometry, location: null); } - private Task ValidateTexture(IResource resource) + private async Task ValidateTexture(IResource resource) { - using var handle = assetRegistry.LoadTexture(resource.Path, AssetPriority.Synchronous); - return Task.CompletedTask; + using var handle = assetRegistry.LoadTexture(resource.Path, AssetPriority.High); + await handle.GetAsync(CancellationToken.None); } private static void ValidateScene(IResource resource) @@ -142,17 +142,17 @@ private static void ValidateScene(IResource resource) scene.Read(stream); } - private Task ValidateActor(IResource resource) + private async Task ValidateActor(IResource resource) { using var handle = assetRegistry.LoadActor( Path.GetFileNameWithoutExtension(resource.Name), - AssetPriority.Synchronous); - return Task.CompletedTask; + AssetPriority.High); + await handle.GetAsync(CancellationToken.None); } - private Task ValidateEffect(IResource resource) + private async Task ValidateEffect(IResource resource) { - using var handle = assetRegistry.LoadEffectCombiner(resource.Path, AssetPriority.Synchronous); - return Task.CompletedTask; + using var handle = assetRegistry.LoadEffectCombiner(resource.Path, AssetPriority.High); + await handle.GetAsync(CancellationToken.None); } } From 9dc815a91aa9643c75032fc68ee943c42c074200 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 6 Aug 2025 13:57:55 +0200 Subject: [PATCH 56/64] Report validation messages as diagnostics --- zzre.core/Diagnostic.cs | 254 +++++++++++++++++++++++++++ zzre/Program.Validation.cs | 4 + zzre/tools/validation/Diagnostics.cs | 18 ++ zzre/tools/validation/Validator.cs | 36 +++- 4 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 zzre.core/Diagnostic.cs create mode 100644 zzre/tools/validation/Diagnostics.cs 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/Program.Validation.cs b/zzre/Program.Validation.cs index 1fd9536b..5566fcec 100644 --- a/zzre/Program.Validation.cs +++ b/zzre/Program.Validation.cs @@ -47,6 +47,10 @@ private static void HandleValidation(InvocationContext ctx) }, cancellationSource.Token).WaitAndRethrow(); WriteProgress(); + foreach (var diagnostic in validator.Diagnostics) + diagnostic.WriteToConsole(); + validator.LogSummary(); + CommonCleanup(diContainer); void WriteProgress() => 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 index 77b21476..0c58e9a9 100644 --- a/zzre/tools/validation/Validator.cs +++ b/zzre/tools/validation/Validator.cs @@ -9,6 +9,7 @@ using Serilog; using zzio.scn; using zzio.vfs; +using static zzre.Diagnostics; namespace zzre.validation; @@ -18,6 +19,7 @@ public class Validator(ITagContainer diContainer) 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; @@ -25,6 +27,7 @@ public class Validator(ITagContainer diContainer) 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); @@ -56,7 +59,25 @@ public async Task Run(CancellationToken ct) resourceTraversalBlock.Complete(); await processResourceBlock.Completion; stopwatch.Stop(); - logger.Information("Validation of {ProcessedFileCount} ({FaultyFileCount} faulty) resources finished in {Elapsed}", processedFileCount, faultyFileCount, stopwatch.Elapsed); + + 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) @@ -87,7 +108,8 @@ private async Task ValidateResource(IResource resource) bool wasIgnored = false; try { - switch (Path.GetExtension(resource.Name).ToLowerInvariant()) + var ext = Path.GetExtension(resource.Name).ToLowerInvariant(); + switch (ext) { case ".bsp": await ValidateWorld(resource); break; case ".scn": ValidateScene(resource); break; @@ -97,14 +119,14 @@ private async Task ValidateResource(IResource resource) case ".ed": await ValidateEffect(resource); break; case ".dff": await ValidateClump(resource); break; default: - logger.Verbose("Ignored file (due to extension): {FileName}", resource.Name); + AddDiagnostic(ValIgnoredDueToExtension(resource.Path.ToString(), ext)); wasIgnored = true; break; } } catch (Exception e) { - logger.Error("Exception when processing {Resource}: {Exception}", resource.Name, e); + AddDiagnostic(ValGeneralException(resource.Path.ToString(), e)); Interlocked.Increment(ref faultyFileCount); } finally @@ -155,4 +177,10 @@ 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); + } } From 55ab299b700a130e397c9eb727347d5cdb65a11b Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 8 Aug 2025 10:25:38 +0200 Subject: [PATCH 57/64] WIP stress tests and fixes --- zzre.core.tests/TestAssetRegistry.cs | 113 ++++++++++++++++++++++- zzre.core/assetregistry/AssetRegistry.cs | 73 ++++++++++++--- 2 files changed, 170 insertions(+), 16 deletions(-) diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index 77b9fde6..accf4948 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -2,9 +2,11 @@ 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; @@ -13,7 +15,7 @@ namespace zzre.tests; [TestFixture(TaskContinuationOptions.None)] [TestFixture(TaskContinuationOptions.RunContinuationsAsynchronously)] [TestFixture(TaskContinuationOptions.ExecuteSynchronously)] -[CancelAfter(3000), SingleThreaded] +[CancelAfter(10000), SingleThreaded] public class TestAssetRegistry { private interface ITestAsset : IAsset @@ -58,6 +60,7 @@ public static async Task> LoadAsync(IAssetRegi 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 @@ -89,6 +92,8 @@ public static Task> LoadAsync(IAssetRegistry registry, 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(); } @@ -555,6 +560,22 @@ public async Task LoadLow_DelRefBeforeLoad(CancellationToken ct) 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) { @@ -766,6 +787,61 @@ await Task.Run(() => } } + [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( + Task.Run(() => SingularStress(1), ct), + Task.Run(() => SingularStress(2), ct), + Task.Run(() => SingularStress(3), ct), + Task.Run(() => SingularStress(4), ct), + Task.Run(() => SingularStress(5), ct), + Task.Run(() => SingularStress(6), ct), + Task.Run(() => SingularStress(7), ct), + Task.Run(() => SingularStress(8), 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) { @@ -795,6 +871,41 @@ public async Task DisposeAsset_DuringLowLoad(CancellationToken ct) 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 { diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 57dbb761..97dd929b 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -100,19 +100,23 @@ public void Dispose() logger.Verbose("Finished disposing registry"); } - private void DisposeAssetState(AssetState state) + private void DisposeAssetObject(bool needsMainThreadDisposal, IDisposable? assetObject) { - if (IsMainThread || !state.NeedsMainThreadDisposal) + if (IsMainThread || !needsMainThreadDisposal) { localStats.OnAssetRemoved(); - state.Asset?.Dispose(); + assetObject?.Dispose(); } - else if (state.Asset is not null) + else if (assetObject is not null) { - var success = assetsToDispose.Writer.TryWrite(state.Asset); + 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; } @@ -245,7 +249,8 @@ public AssetHandle Load(in TInfo info, AssetPriority prio } break; case AssetPriority.High: - Task.Run(() => handle.GetAsync(Cancellation), Cancellation); + Console.WriteLine($"Queue {info}"); + Task.Run(() => TryStartLoad(handle.AssetId), Cancellation); break; case AssetPriority.Low: var success = assetsToStart.Writer.TryWrite(assetId); @@ -313,35 +318,70 @@ private static void SanityCheckSharedAsset(Type expectedType, AssetState asset) Debug.Assert(actualType.IsAssignableTo(expectedType), "Asset type mismatch, is this a GUID conflict?"); } + private async Task TryStartLoad(Guid assetId) + { + AssetState? assetState = null; + await LockSemaphoreAsync(Cancellation); + try + { + assetState = assets.GetValueOrDefault(assetId); + if (assetState is null or { RefCount: <= 0 }) + { + // 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 + Console.WriteLine($"not started disposed {assetId}"); + return; + } + } + finally + { + semaphore.Release(); + } + + await assetState.LoadLazy.WithCancellation(Cancellation); + } + private async Task LoadAsset(TInfo info, Guid assetId) where TInfo : struct, IEquatable where TAsset : class, IAsset { // Due to AsyncLazy we can flow exceptions outside this method + Console.WriteLine($"before load {info}"); + // Load asset var asset = (await TAsset.LoadAsync(this, assetId, info, Cancellation)).Asset; Debug.Assert(asset.Registry == this); CheckRegistryDisposal(); + Console.WriteLine($"after load 2{info}"); + // Propagate assets into registry state + AssetState? assetState = null; await LockSemaphoreAsync(Cancellation); try { - var assetState = assets.GetValueOrDefault(assetId); - ObjectDisposedException.ThrowIf(assetState is null or { RefCount: <= 0 }, typeof(AssetState)); - localStats.OnAssetLoaded(); - } - catch - { - asset.Dispose(); - throw; + assetState = assets.GetValueOrDefault(assetId); + if (assetState is null or { RefCount: <= 0 }) + assetState = null; + else + localStats.OnAssetLoaded(); } finally { semaphore.Release(); } + if (assetState is null) + { + Console.WriteLine($"dispose after load {info}"); + // handle was disposed during load, now dispose the asset itself + DisposeAssetObject(TAsset.NeedsMainThreadDisposal, asset); + ObjectDisposedException.ThrowIf(true, typeof(TAsset)); + } + + Console.WriteLine($"return {info}"); + CheckRegistryDisposal(); return asset; @@ -350,7 +390,10 @@ void CheckRegistryDisposal() { if (WasDisposed) { - asset.Dispose(); + Console.WriteLine($"Dispose because registry disposal {info}"); + 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(); From 9280881a217b00559c36fda26654056dda5c1bf0 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 20 Aug 2025 21:34:04 +0200 Subject: [PATCH 58/64] Abstract lock --- zzre.core/assetregistry/AssetRegistry.cs | 246 ++++++++++------------- 1 file changed, 110 insertions(+), 136 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 97dd929b..255a19f8 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -31,6 +31,47 @@ public IDisposable? Asset public string? Name; } +public interface IAssetRegistryLock : IDisposable +{ + Releaser Wait(TimeSpan timeout, CancellationToken ct); + Task WaitAsync(TimeSpan timeout, CancellationToken ct); + + protected 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) => + semaphore.Wait(timeout, ct) ? new(this) : default; + + public Task WaitAsync(TimeSpan timeout, CancellationToken ct) => + IAssetRegistryLock.Releaser.ConvertFromBoolTask(semaphore.WaitAsync(timeout, ct), this, ct); +} + public class AssetRegistry : IAssetRegistryInternal { private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(3); @@ -47,7 +88,7 @@ public class AssetRegistry : IAssetRegistryInternal private readonly Channel assetsToDispose = Channel.CreateUnbounded(ChannelOptions); private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); - private readonly SemaphoreSlim semaphore = new(1, 1); + private readonly IAssetRegistryLock mainLock = new SemaphoreAssetLock(); private readonly ILogger logger; private readonly int mainThreadId; private readonly Dictionary> applyActionCaster = []; @@ -81,7 +122,7 @@ public void Dispose() { if (WasDisposed) return; - if (semaphore.Wait(LockTimeout, Cancellation)) + if (mainLock.Wait(LockTimeout, Cancellation)) logger.Warning("AssetRegistry could not lock during dispose, going ahead nonetheless"); cancellationSource.Cancel(); foreach (var asset in assets.Values) @@ -95,7 +136,7 @@ public void Dispose() assetsToStart.Writer.TryComplete(); DisposeOldAssets(); // after current assets in case we add something into it (we shouldn't) - semaphore.Dispose(); + mainLock.Dispose(); cancellationSource.Dispose(); logger.Verbose("Finished disposing registry"); } @@ -122,38 +163,35 @@ private void DisposeAssetState(AssetState state) } [ExcludeFromCodeCoverage] // we cannot reasonably check for semaphore failure - private void LockSemaphore() + private IAssetRegistryLock.Releaser LockSemaphore() { - if (!semaphore.Wait(LockTimeout, Cancellation)) + var releaser = mainLock.Wait(LockTimeout, Cancellation); + 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) + private async Task LockSemaphoreAsync(CancellationToken ct) { - if (!await semaphore.WaitAsync(LockTimeout, Cancellation)) + var releaser = await mainLock.WaitAsync(LockTimeout, Cancellation); // NOT LINKED TO CT + if (!releaser) throw new InvalidOperationException("Could not lock asset registry"); + return releaser; } void IAssetRegistryInternal.AddRef(Guid assetId) { ObjectDisposedException.ThrowIf(WasDisposed, typeof(IAssetRegistry)); - LockSemaphore(); - try - { - ObjectDisposedException.ThrowIf(!TryAddRefUnsafe(assetId), typeof(IAsset)); - } - finally - { - semaphore.Release(); - } + using var _ = LockSemaphore(); + ObjectDisposedException.ThrowIf(!TryAddRefUnsafe(assetId), typeof(IAsset)); } private bool TryAddRefUnsafe(Guid assetId) { Debug.Assert(!WasDisposed); - Debug.Assert(semaphore.CurrentCount == 0); + //Debug.Assert(semaphore.CurrentCount == 0); var assetState = assets.GetValueOrDefault(assetId); if (assetState is null || assetState.RefCount <= 0) return false; @@ -165,21 +203,14 @@ private bool TryAddRefUnsafe(Guid assetId) void IAssetRegistryInternal.DelRef(Guid assetId) { if (WasDisposed) return; // Ignore out-of-order deletion, all assets are already dead - LockSemaphore(); - try - { - DelRefUnsafe(assetId); - } - finally - { - semaphore.Release(); - } + using var releaser = LockSemaphore(); + DelRefUnsafe(assetId); } private void DelRefUnsafe(Guid assetId) { Debug.Assert(!WasDisposed); - Debug.Assert(semaphore.CurrentCount == 0); + //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 @@ -193,31 +224,17 @@ private void DelRefUnsafe(Guid assetId) AsyncLazy IAssetRegistryInternal.GetAsset(Guid assetId) { AssetState asset; - LockSemaphore(); - try - { - ObjectDisposedException.ThrowIf(!assets.TryGetValue(assetId, out asset!), nameof(IAssetHandle)); - } - finally - { - semaphore.Release(); - } + using var releaser = LockSemaphore(); + ObjectDisposedException.ThrowIf(!assets.TryGetValue(assetId, out asset!), nameof(IAssetHandle)); return asset.LoadLazy; } void IAssetRegistryInternal.CheckType(Guid assetId, Type type) { - LockSemaphore(); - try - { - 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}"); - } - finally - { - semaphore.Release(); - } + 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) @@ -265,49 +282,43 @@ public AssetHandle Load(in TInfo info, AssetPriority prio where TInfo : struct, IEquatable where TAsset : class, IAsset { - LockSemaphore(); - try - { - // Determine Asset ID - Guid assetId; - if (TAsset.Locality is AssetLocality.Unique) - { - do - { - assetId = Guid.NewGuid(); - } while (assets.ContainsKey(assetId)); // just paranoid... - } - else - assetId = TAsset.InfoToAssetId(info); + using var releaser = LockSemaphore(); - // Check previous asset state - if (assets.TryGetValue(assetId, out var assetState) && assetState.RefCount > 0) + // Determine Asset ID + Guid assetId; + if (TAsset.Locality is AssetLocality.Unique) + { + do { - SanityCheckSharedAsset(typeof(TAsset), assetState); - assetState.RefCount++; - if (assetState.Asset is null && (int)priority < (int)assetState.Priority) - assetState.Priority = priority; - return (assetId, assetState); - } + assetId = Guid.NewGuid(); + } while (assets.ContainsKey(assetId)); // just paranoid... + } + else + assetId = TAsset.InfoToAssetId(info); - // 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(); + // 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); } - finally + + // Create new asset state + TInfo infoCopy = info; + assetState = new() { - semaphore.Release(); - } + 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); } [Conditional("DEBUG")] @@ -321,8 +332,7 @@ private static void SanityCheckSharedAsset(Type expectedType, AssetState asset) private async Task TryStartLoad(Guid assetId) { AssetState? assetState = null; - await LockSemaphoreAsync(Cancellation); - try + using (await LockSemaphoreAsync(Cancellation)) { assetState = assets.GetValueOrDefault(assetId); if (assetState is null or { RefCount: <= 0 }) @@ -333,10 +343,6 @@ private async Task TryStartLoad(Guid assetId) return; } } - finally - { - semaphore.Release(); - } await assetState.LoadLazy.WithCancellation(Cancellation); } @@ -358,8 +364,7 @@ private async Task LoadAsset(TInfo info, Guid assetI // Propagate assets into registry state AssetState? assetState = null; - await LockSemaphoreAsync(Cancellation); - try + using (await LockSemaphoreAsync(Cancellation)) { assetState = assets.GetValueOrDefault(assetId); if (assetState is null or { RefCount: <= 0 }) @@ -367,10 +372,6 @@ private async Task LoadAsset(TInfo info, Guid assetI else localStats.OnAssetLoaded(); } - finally - { - semaphore.Release(); - } if (assetState is null) { @@ -408,22 +409,15 @@ public bool TryGet(Guid assetId, out AssetHandle handle) return true; handle = default; - LockSemaphore(); - try - { - if (!assets.TryGetValue(assetId, out var assetState) || - assetState.RefCount < 1 || - !assetState.AssetType.IsAssignableTo(typeof(TAsset))) - return false; + 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; - } - finally - { - semaphore.Release(); - } + assetState.RefCount++; + handle = new(this, assetId); + return true; } public void Update() => @@ -435,8 +429,7 @@ public void Update(int maxLowPrioAssets) if (!IsMainThread) throw new InvalidOperationException("Low batch scheduling is only allowed on the main thread"); - LockSemaphore(); - try + using (LockSemaphore()) { DisposeOldAssets(); @@ -471,10 +464,6 @@ public void Update(int maxLowPrioAssets) } } } - finally - { - semaphore.Release(); - } // We can safely access applyActionsBackup as we are on the main thread var exceptions = new List(); @@ -493,8 +482,7 @@ public void Update(int maxLowPrioAssets) } // Remove reference we added earlier - LockSemaphore(); - try + using (LockSemaphore()) { foreach (var (assetId, _, _, _) in applyActionsBackup) { @@ -502,10 +490,6 @@ public void Update(int maxLowPrioAssets) DelRefUnsafe(assetId); } } - finally - { - semaphore.Release(); - } applyActionsBackup.Clear(); if (exceptions.Count > 0) @@ -515,7 +499,7 @@ public void Update(int maxLowPrioAssets) private void DisposeOldAssets() { Debug.Assert(IsMainThread); - Debug.Assert(semaphore.CurrentCount == 0); + //Debug.Assert(semaphore.CurrentCount == 0); while (assetsToDispose.Reader.TryRead(out var asset)) { asset.Dispose(); @@ -539,8 +523,7 @@ public void Apply(AssetHandle handle, Action bool shouldBeExecutedNow = false; - LockSemaphore(); - try + using (LockSemaphore()) { if (!applyActionCaster.ContainsKey(typeof(TAsset))) { @@ -561,10 +544,6 @@ public void Apply(AssetHandle handle, Action if (!shouldBeExecutedNow) applyActions.Add(new(handle.AssetId, assets[handle.AssetId].Tag, typeof(TAsset), action)); } - finally - { - semaphore.Release(); - } // Fast-path: no queueing if (shouldBeExecutedNow) @@ -582,8 +561,7 @@ public void Apply(AssetHandle handle, Action public void CopyDebugInfo(List infos) { - LockSemaphore(); - try + using (LockSemaphore()) { infos.Clear(); infos.EnsureCapacity(assets.Count); @@ -603,9 +581,5 @@ public void CopyDebugInfo(List infos) )); } } - finally - { - semaphore.Release(); - } } } From bbbd2d9594cb9f18580006cfda08ad42bd8c3612 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 20 Aug 2025 21:36:36 +0200 Subject: [PATCH 59/64] Link tokens together in LockSemaphoreAsync --- zzre.core/assetregistry/AssetRegistry.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 255a19f8..e31f24d2 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -175,7 +175,8 @@ private IAssetRegistryLock.Releaser LockSemaphore() [ExcludeFromCodeCoverage] private async Task LockSemaphoreAsync(CancellationToken ct) { - var releaser = await mainLock.WaitAsync(LockTimeout, Cancellation); // NOT LINKED TO CT + using var cts = CancellationTokenSource.CreateLinkedTokenSource(Cancellation, ct); + var releaser = await mainLock.WaitAsync(LockTimeout, cts.Token); // NOT LINKED TO CT if (!releaser) throw new InvalidOperationException("Could not lock asset registry"); return releaser; From 053418411472522a723ded0f6cdc875775012309 Mon Sep 17 00:00:00 2001 From: Helco Date: Wed, 20 Aug 2025 22:11:07 +0200 Subject: [PATCH 60/64] Add tracking asset lock and a bit more stress --- zzre.core.tests/TestAssetRegistry.cs | 12 +-- zzre.core/assetregistry/AssetRegistry.cs | 47 +------- zzre.core/assetregistry/IAssetRegistryLock.cs | 102 ++++++++++++++++++ 3 files changed, 108 insertions(+), 53 deletions(-) create mode 100644 zzre.core/assetregistry/IAssetRegistryLock.cs diff --git a/zzre.core.tests/TestAssetRegistry.cs b/zzre.core.tests/TestAssetRegistry.cs index accf4948..a4e42c66 100644 --- a/zzre.core.tests/TestAssetRegistry.cs +++ b/zzre.core.tests/TestAssetRegistry.cs @@ -806,15 +806,9 @@ public async Task DisposeAsset_StressBeforeHighLoad(CancellationToken ct) { Console.WriteLine("started run"); using var global = new AssetRegistry(DI); - await Task.WhenAll( - Task.Run(() => SingularStress(1), ct), - Task.Run(() => SingularStress(2), ct), - Task.Run(() => SingularStress(3), ct), - Task.Run(() => SingularStress(4), ct), - Task.Run(() => SingularStress(5), ct), - Task.Run(() => SingularStress(6), ct), - Task.Run(() => SingularStress(7), ct), - Task.Run(() => SingularStress(8), ct) + await Task.WhenAll(Enumerable + .Range(1, 50) + .Select(i => Task.Run(() => SingularStress(i), ct)) ).WaitAsync(ct); Console.WriteLine("ended run"); diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index e31f24d2..dc80c1b4 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -31,47 +31,6 @@ public IDisposable? Asset public string? Name; } -public interface IAssetRegistryLock : IDisposable -{ - Releaser Wait(TimeSpan timeout, CancellationToken ct); - Task WaitAsync(TimeSpan timeout, CancellationToken ct); - - protected 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) => - semaphore.Wait(timeout, ct) ? new(this) : default; - - public Task WaitAsync(TimeSpan timeout, CancellationToken ct) => - IAssetRegistryLock.Releaser.ConvertFromBoolTask(semaphore.WaitAsync(timeout, ct), this, ct); -} - public class AssetRegistry : IAssetRegistryInternal { private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(3); @@ -88,7 +47,7 @@ public class AssetRegistry : IAssetRegistryInternal private readonly Channel assetsToDispose = Channel.CreateUnbounded(ChannelOptions); private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); - private readonly IAssetRegistryLock mainLock = new SemaphoreAssetLock(); + private readonly IAssetRegistryLock mainLock = new TrackingAssetLock(new SemaphoreAssetLock()); private readonly ILogger logger; private readonly int mainThreadId; private readonly Dictionary> applyActionCaster = []; @@ -175,8 +134,8 @@ private IAssetRegistryLock.Releaser LockSemaphore() [ExcludeFromCodeCoverage] private async Task LockSemaphoreAsync(CancellationToken ct) { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(Cancellation, ct); - var releaser = await mainLock.WaitAsync(LockTimeout, cts.Token); // NOT LINKED TO CT + // using var cts = CancellationTokenSource.CreateLinkedTokenSource(Cancellation, ct); + var releaser = await mainLock.WaitAsync(LockTimeout, Cancellation); // NOT LINKED TO CT if (!releaser) throw new InvalidOperationException("Could not lock asset registry"); return releaser; diff --git a/zzre.core/assetregistry/IAssetRegistryLock.cs b/zzre.core/assetregistry/IAssetRegistryLock.cs new file mode 100644 index 00000000..dddbfe2d --- /dev/null +++ b/zzre.core/assetregistry/IAssetRegistryLock.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace zzre; + +public interface IAssetRegistryLock : IDisposable +{ + Releaser Wait(TimeSpan timeout, CancellationToken ct); + Task WaitAsync(TimeSpan timeout, CancellationToken ct); + + 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) => + semaphore.Wait(timeout, ct) ? new(this) : default; + + public Task WaitAsync(TimeSpan timeout, CancellationToken ct) => + IAssetRegistryLock.Releaser.ConvertFromBoolTask(semaphore.WaitAsync(timeout, ct), this, ct); +} + +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) + { + var next = new StackFrame(1).ToString(); + var releaser = inner.Wait(timeout, ct); + try + { + if (releaser) + Interlocked.Exchange(ref last, next); + } + catch + { + Debug.Assert(false); + releaser.Dispose(); + throw; + } + return releaser; + } + + public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) + { + var next = new StackFrame(1).ToString(); + var releaser = await inner.WaitAsync(timeout, ct); + try + { + if (releaser) + Interlocked.Exchange(ref last, next); + } + catch + { + Debug.Assert(false); + releaser.Dispose(); + throw; + } + return releaser; + } +} From 6e4361eab1d17b5d737cbfaf8791ba8f59d7b949 Mon Sep 17 00:00:00 2001 From: Helco Date: Thu, 21 Aug 2025 07:38:29 +0200 Subject: [PATCH 61/64] Add better context to tracking lock --- zzre.core/assetregistry/AssetRegistry.cs | 11 +++++---- zzre.core/assetregistry/IAssetRegistryLock.cs | 23 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index dc80c1b4..a97ef6ca 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -122,9 +123,9 @@ private void DisposeAssetState(AssetState state) } [ExcludeFromCodeCoverage] // we cannot reasonably check for semaphore failure - private IAssetRegistryLock.Releaser LockSemaphore() + private IAssetRegistryLock.Releaser LockSemaphore([CallerMemberName] string context = "") { - var releaser = mainLock.Wait(LockTimeout, Cancellation); + var releaser = mainLock.Wait(LockTimeout, Cancellation, context); if (!releaser) throw new InvalidOperationException("Could not lock asset registry"); return releaser; @@ -132,10 +133,10 @@ private IAssetRegistryLock.Releaser LockSemaphore() } [ExcludeFromCodeCoverage] - private async Task LockSemaphoreAsync(CancellationToken ct) + private async Task LockSemaphoreAsync(CancellationToken ct, [CallerMemberName] string context = "") { - // using var cts = CancellationTokenSource.CreateLinkedTokenSource(Cancellation, ct); - var releaser = await mainLock.WaitAsync(LockTimeout, Cancellation); // NOT LINKED TO CT + 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; diff --git a/zzre.core/assetregistry/IAssetRegistryLock.cs b/zzre.core/assetregistry/IAssetRegistryLock.cs index dddbfe2d..8f047027 100644 --- a/zzre.core/assetregistry/IAssetRegistryLock.cs +++ b/zzre.core/assetregistry/IAssetRegistryLock.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -7,8 +8,8 @@ namespace zzre; public interface IAssetRegistryLock : IDisposable { - Releaser Wait(TimeSpan timeout, CancellationToken ct); - Task WaitAsync(TimeSpan timeout, CancellationToken ct); + 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 @@ -39,10 +40,10 @@ public sealed class SemaphoreAssetLock : IAssetRegistryLock void IAssetRegistryLock.Release() => semaphore.Release(); - public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct) => + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, string _) => semaphore.Wait(timeout, ct) ? new(this) : default; - public Task WaitAsync(TimeSpan timeout, CancellationToken ct) => + public Task WaitAsync(TimeSpan timeout, CancellationToken ct, string _) => IAssetRegistryLock.Releaser.ConvertFromBoolTask(semaphore.WaitAsync(timeout, ct), this, ct); } @@ -64,14 +65,13 @@ void IAssetRegistryLock.Release() last = null; } - public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct) + public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, string context) { - var next = new StackFrame(1).ToString(); - var releaser = inner.Wait(timeout, ct); + var releaser = inner.Wait(timeout, ct, context); try { if (releaser) - Interlocked.Exchange(ref last, next); + Interlocked.Exchange(ref last, context); } catch { @@ -82,14 +82,13 @@ public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct) return releaser; } - public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) + public async Task WaitAsync(TimeSpan timeout, CancellationToken ct, string context) { - var next = new StackFrame(1).ToString(); - var releaser = await inner.WaitAsync(timeout, ct); + var releaser = await inner.WaitAsync(timeout, ct, context); try { if (releaser) - Interlocked.Exchange(ref last, next); + Interlocked.Exchange(ref last, context); } catch { From f2ca3c0df4d50947fb81ff7061693a4a9bb35e04 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 22 Aug 2025 20:13:19 +0200 Subject: [PATCH 62/64] Fix double disposal stat --- zzre.core/assetregistry/AssetRegistry.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index a97ef6ca..2dbc43be 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -103,12 +103,14 @@ public void Dispose() private void DisposeAssetObject(bool needsMainThreadDisposal, IDisposable? assetObject) { + if (assetObject is null) + return; if (IsMainThread || !needsMainThreadDisposal) { localStats.OnAssetRemoved(); - assetObject?.Dispose(); + assetObject.Dispose(); } - else if (assetObject is not null) + else { var success = assetsToDispose.Writer.TryWrite(assetObject); Debug.Assert(success); From 29279ca761cb58869a3db720fd58740b84ba2d45 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 22 Aug 2025 20:17:24 +0200 Subject: [PATCH 63/64] Add equally broken MonitorAssetLock and DotNextAsyncLock --- Directory.Build.props | 2 +- extern/Mlang | 2 +- zzre.core/assetregistry/AssetRegistry.cs | 2 +- zzre.core/assetregistry/IAssetRegistryLock.cs | 52 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) 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/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index 2dbc43be..d4082bd1 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -48,7 +48,7 @@ public class AssetRegistry : IAssetRegistryInternal private readonly Channel assetsToDispose = Channel.CreateUnbounded(ChannelOptions); private readonly Dictionary assets = []; private readonly CancellationTokenSource cancellationSource = new(); - private readonly IAssetRegistryLock mainLock = new TrackingAssetLock(new SemaphoreAssetLock()); + private readonly IAssetRegistryLock mainLock = new TrackingAssetLock(new DotNextAsyncAssetLock()); private readonly ILogger logger; private readonly int mainThreadId; private readonly Dictionary> applyActionCaster = []; diff --git a/zzre.core/assetregistry/IAssetRegistryLock.cs b/zzre.core/assetregistry/IAssetRegistryLock.cs index 8f047027..27a8fe69 100644 --- a/zzre.core/assetregistry/IAssetRegistryLock.cs +++ b/zzre.core/assetregistry/IAssetRegistryLock.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using DotNext.Threading; +using DotNext.Threading.Tasks; namespace zzre; @@ -47,6 +49,56 @@ public IAssetRegistryLock.Releaser Wait(TimeSpan timeout, CancellationToken ct, 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; From b16764e7fa3bf09457132fde843278ca9c012430 Mon Sep 17 00:00:00 2001 From: Helco Date: Fri, 22 Aug 2025 20:31:20 +0200 Subject: [PATCH 64/64] Remove console writes --- zzre.core/assetregistry/AssetRegistry.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/zzre.core/assetregistry/AssetRegistry.cs b/zzre.core/assetregistry/AssetRegistry.cs index d4082bd1..80567fc1 100644 --- a/zzre.core/assetregistry/AssetRegistry.cs +++ b/zzre.core/assetregistry/AssetRegistry.cs @@ -229,7 +229,6 @@ public AssetHandle Load(in TInfo info, AssetPriority prio } break; case AssetPriority.High: - Console.WriteLine($"Queue {info}"); Task.Run(() => TryStartLoad(handle.AssetId), Cancellation); break; case AssetPriority.Low: @@ -302,7 +301,6 @@ private async Task TryStartLoad(Guid assetId) { // 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 - Console.WriteLine($"not started disposed {assetId}"); return; } } @@ -316,15 +314,11 @@ private async Task LoadAsset(TInfo info, Guid assetI { // Due to AsyncLazy we can flow exceptions outside this method - Console.WriteLine($"before load {info}"); - // Load asset var asset = (await TAsset.LoadAsync(this, assetId, info, Cancellation)).Asset; Debug.Assert(asset.Registry == this); CheckRegistryDisposal(); - Console.WriteLine($"after load 2{info}"); - // Propagate assets into registry state AssetState? assetState = null; using (await LockSemaphoreAsync(Cancellation)) @@ -338,14 +332,11 @@ private async Task LoadAsset(TInfo info, Guid assetI if (assetState is null) { - Console.WriteLine($"dispose after load {info}"); // handle was disposed during load, now dispose the asset itself DisposeAssetObject(TAsset.NeedsMainThreadDisposal, asset); ObjectDisposedException.ThrowIf(true, typeof(TAsset)); } - Console.WriteLine($"return {info}"); - CheckRegistryDisposal(); return asset; @@ -354,7 +345,6 @@ void CheckRegistryDisposal() { if (WasDisposed) { - Console.WriteLine($"Dispose because registry disposal {info}"); if (!TAsset.NeedsMainThreadDisposal) asset.Dispose(); // otherwise we are in a predicament and my decision is to leak a couple assets