diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 0d91a4b75..27771116e 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -9,6 +9,7 @@ namespace Dalamud; /// Any asset can cease to exist at any point, even if the enum value exists.
/// Either ship your own assets, or be prepared for errors. /// +// Implementation notes: avoid specifying numbers too high here. Lookup table is currently implemented as an array. public enum DalamudAsset { /// diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs index 905e8ed23..f9dae288b 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawResult.cs @@ -5,7 +5,7 @@ using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Interface.ImGuiSeStringRenderer; -/// Represents the result of n rendered interactable SeString. +/// Represents the result of a rendered interactable SeString. public ref struct SeStringDrawResult { private Payload? lazyPayload; diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index c71d83fe8..5f9925ed3 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -171,17 +171,14 @@ internal abstract class SharedImmediateTexture /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IDalamudTextureWrap GetWrapOrEmpty() => this.GetWrapOrDefault(Service.Get().Empty4X4); + public IDalamudTextureWrap GetWrapOrEmpty() => + this.TryGetWrap(out var texture, out _) ? texture : Service.Get().Empty4X4; /// [return: NotNullIfNotNull(nameof(defaultWrap))] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap) - { - if (!this.TryGetWrap(out var texture, out _)) - texture = null; - return texture ?? defaultWrap; - } + public IDalamudTextureWrap? GetWrapOrDefault(IDalamudTextureWrap? defaultWrap) => + this.TryGetWrap(out var texture, out _) ? texture : defaultWrap; /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs index 8b0516e03..7d6ff8580 100644 --- a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs @@ -37,7 +37,11 @@ public abstract class ForwardingTextureWrap : IDalamudTextureWrap public Vector2 Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(this.Width, this.Height); + get + { + var wrap = this.GetWrap(); + return new(wrap.Width, wrap.Height); + } } /// diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs index 0dd5c9f25..3bb984be8 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs @@ -1,20 +1,13 @@ -using Dalamud.Interface.Internal; - namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that ignores calls. -internal class DisposeSuppressingTextureWrap : ForwardingTextureWrap +/// The inner wrap. +internal class DisposeSuppressingTextureWrap(IDalamudTextureWrap innerWrap) : ForwardingTextureWrap { - private readonly IDalamudTextureWrap innerWrap; - - /// Initializes a new instance of the class. - /// The inner wrap. - public DisposeSuppressingTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; - /// protected override bool TryGetWrap(out IDalamudTextureWrap? wrap) { - wrap = this.innerWrap; + wrap = innerWrap; return true; } } diff --git a/Dalamud/Storage/Assets/DalamudAssetExtensions.cs b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs index 9052a1c6d..4fe72240b 100644 --- a/Dalamud/Storage/Assets/DalamudAssetExtensions.cs +++ b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs @@ -1,46 +1,37 @@ -using System.Collections.Frozen; -using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.Utility; namespace Dalamud.Storage.Assets; -/// -/// Extension methods for . -/// +/// Extension methods for . public static class DalamudAssetExtensions { - private static readonly FrozenDictionary AttributeCache = CreateCache(); + private static readonly DalamudAssetAttribute EmptyAttribute = new(DalamudAssetPurpose.Empty, null, false); + private static readonly DalamudAssetAttribute[] AttributeCache = CreateCache(); - /// - /// Gets the purpose. - /// + /// Gets the purpose. /// The asset. /// The purpose. - public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) - { - return GetAssetAttribute(asset)?.Purpose ?? DalamudAssetPurpose.Empty; - } + public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) => asset.GetAssetAttribute().Purpose; - /// - /// Gets the attribute. - /// + /// Gets the attribute. /// The asset. /// The attribute. - internal static DalamudAssetAttribute? GetAssetAttribute(this DalamudAsset asset) + internal static DalamudAssetAttribute GetAssetAttribute(this DalamudAsset asset) => + (int)asset < 0 || (int)asset >= AttributeCache.Length + ? EmptyAttribute + : Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(AttributeCache), (int)asset); + + private static DalamudAssetAttribute[] CreateCache() { - return AttributeCache.GetValueOrDefault(asset); - } - - private static FrozenDictionary CreateCache() - { - var dict = new Dictionary(); - - foreach (var asset in Enum.GetValues()) - { - dict.Add(asset, asset.GetAttribute()); - } - - return dict.ToFrozenDictionary(); + var assets = Enum.GetValues(); + var table = new DalamudAssetAttribute[assets.Max(x => (int)x) + 1]; + table.AsSpan().Fill(EmptyAttribute); + foreach (var asset in assets) + table[(int)asset] = asset.GetAttribute() ?? EmptyAttribute; + return table; } } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index f750de64a..6fe26b90b 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps.Internal; @@ -36,10 +37,9 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud private const int DownloadAttemptCount = 10; private const int RenameAttemptCount = 10; - private readonly object syncRoot = new(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly Dictionary?> fileStreams; - private readonly Dictionary?> textureWraps; + private readonly Task?[] fileStreams; + private readonly Task?[] textureWraps; private readonly Dalamud dalamud; private readonly HappyHttpClient httpClient; private readonly string localSourceDirectory; @@ -59,18 +59,18 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud Directory.CreateDirectory(this.localSourceDirectory); this.scopedFinalizer.Add(this.cancellationTokenSource = new()); - this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); - this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + var numDalamudAssetSlots = Enum.GetValues().Max(x => (int)x) + 1; + this.fileStreams = new Task?[numDalamudAssetSlots]; + this.textureWraps = new Task?[numDalamudAssetSlots]; // Block until all the required assets to be ready. var loadTimings = Timings.Start("DAM LoadAll"); registerStartupBlocker( Task.WhenAll( Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAssetAttribute()?.Required is true) + .Where(static x => x.GetAssetAttribute() is { Required: true, Data: null }) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + .Select(static x => x.ToContentDisposedTask())) .ContinueWith( r => { @@ -80,13 +80,13 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud .Unwrap(), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); + // Begin preloading optional(non-required) assets. Task.WhenAll( Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAssetAttribute()?.Required is false) + .Where(static x => x.GetAssetAttribute() is { Required: false, Data: null }) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask(true))) - .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); + .Select(static x => x.ToContentDisposedTask(true))) + .ContinueWith(static r => Log.Verbose($"Optional assets load state: {r}")); } /// @@ -98,77 +98,97 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud /// void IInternalDisposableService.DisposeService() { - lock (this.syncRoot) - { - if (this.isDisposed) - return; + if (this.isDisposed) + return; - this.isDisposed = true; - } + this.isDisposed = true; this.cancellationTokenSource.Cancel(); Task.WaitAll( Array.Empty() - .Concat(this.fileStreams.Values) - .Concat(this.textureWraps.Values) - .Where(x => x is not null) - .Select(x => x.ContinueWith(r => { _ = r.Exception; })) - .ToArray()); + .Concat(this.fileStreams) + .Concat(this.textureWraps) + .Where(static x => x is not null) + .Select(static x => x.ContinueWith(static r => _ = r.Exception)) + .ToArray()); this.scopedFinalizer.Dispose(); } /// [Pure] public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => - asset.GetAssetAttribute()?.Data is not null - || this.fileStreams[asset]?.IsCompletedSuccessfully is true; + asset.GetAssetAttribute().Data is not null + || this.fileStreams[(int)asset]?.IsCompletedSuccessfully is true; /// [Pure] - public Stream CreateStream(DalamudAsset asset) - { - var s = this.CreateStreamAsync(asset); - s.Wait(); - if (s.IsCompletedSuccessfully) - return s.Result; - if (s.Exception is not null) - throw new AggregateException(s.Exception.InnerExceptions); - throw new OperationCanceledException(); - } + public Stream CreateStream(DalamudAsset asset) => this.CreateStreamAsync(asset).Result; /// [Pure] public Task CreateStreamAsync(DalamudAsset asset) { - if (asset.GetAssetAttribute() is { Data: { } rawData }) - return Task.FromResult(new MemoryStream(rawData, false)); + ObjectDisposedException.ThrowIf(this.isDisposed, this); - Task task; - lock (this.syncRoot) + var attribute = asset.GetAssetAttribute(); + + // The corresponding asset does not exist. + if (attribute.Purpose is DalamudAssetPurpose.Empty) + return Task.FromException(new ArgumentOutOfRangeException(nameof(asset), asset, null)); + + // Special case: raw data is specified from asset definition. + if (attribute.Data is not null) + return Task.FromResult(new MemoryStream(attribute.Data, false)); + + // Range is guaranteed to be satisfied if the asset has a purpose; get the slot for the stream task. + ref var streamTaskRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this.fileStreams), (int)asset); + + // The stream task is already set. + if (streamTaskRef is not null) + return CloneFileStreamAsync(streamTaskRef); + + var tcs = new TaskCompletionSource(); + if (Interlocked.CompareExchange(ref streamTaskRef, tcs.Task, null) is not { } streamTask) { - if (this.isDisposed) - throw new ObjectDisposedException(nameof(DalamudAssetManager)); - - task = this.fileStreams[asset] ??= CreateInnerAsync(); + // The stream task has just been set. Actually start the operation. + // In case it did not correctly finish the task in tcs, set the task to a failed state. + // Do not pass cancellation token here; we always want to touch tcs. + Task.Run( + async () => + { + try + { + tcs.SetResult(await CreateInnerAsync(this, asset)); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, + default); + return CloneFileStreamAsync(tcs.Task); } - return this.TransformImmediate( - task, - x => (Stream)new FileStream( - x.Name, + // Discard the new task, and return the already created task. + tcs.SetCanceled(); + return CloneFileStreamAsync(streamTask); + + static async Task CloneFileStreamAsync(Task fileStreamTask) => + new FileStream( + (await fileStreamTask).Name, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, - FileOptions.Asynchronous | FileOptions.SequentialScan)); + FileOptions.Asynchronous | FileOptions.SequentialScan); - async Task CreateInnerAsync() + static async Task CreateInnerAsync(DalamudAssetManager dam, DalamudAsset asset) { string path; List exceptions = null; - foreach (var name in asset.GetAttributes().Select(x => x.FileName)) + foreach (var name in asset.GetAttributes().Select(static x => x.FileName)) { - if (!File.Exists(path = Path.Combine(this.dalamud.AssetDirectory.FullName, name))) + if (!File.Exists(path = Path.Combine(dam.dalamud.AssetDirectory.FullName, name))) continue; try @@ -177,12 +197,12 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud } catch (Exception e) when (e is not OperationCanceledException) { - exceptions ??= new(); + exceptions ??= []; exceptions.Add(e); } } - if (File.Exists(path = Path.Combine(this.localSourceDirectory, asset.ToString()))) + if (File.Exists(path = Path.Combine(dam.localSourceDirectory, asset.ToString()))) { try { @@ -190,7 +210,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud } catch (Exception e) when (e is not OperationCanceledException) { - exceptions ??= new(); + exceptions ??= []; exceptions.Add(e); } } @@ -211,9 +231,9 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write)) { await url.DownloadAsync( - this.httpClient.SharedHttpClient, + dam.httpClient.SharedHttpClient, tempPathStream, - this.cancellationTokenSource.Token); + dam.cancellationTokenSource.Token); } for (var j = RenameAttemptCount; ; j--) @@ -232,7 +252,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud nameof(DalamudAssetManager), asset, j); - await Task.Delay(1000, this.cancellationTokenSource.Token); + await Task.Delay(1000, dam.cancellationTokenSource.Token); continue; } @@ -255,14 +275,18 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud nameof(DalamudAssetManager), asset, delay); - await Task.Delay(delay * 1000, this.cancellationTokenSource.Token); + await Task.Delay(delay * 1000, dam.cancellationTokenSource.Token); } throw new FileNotFoundException($"Failed to load the asset {asset}.", asset.ToString()); } - catch (Exception e) when (e is not OperationCanceledException) + catch (OperationCanceledException) { - exceptions ??= new(); + throw; + } + catch (Exception e) + { + exceptions ??= []; exceptions.Add(e); try { @@ -272,9 +296,9 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud { // don't care } - } - throw new AggregateException(exceptions); + throw new AggregateException(exceptions); + } } } @@ -296,33 +320,63 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud [Pure] public Task GetDalamudTextureWrapAsync(DalamudAsset asset) { - var purpose = asset.GetPurpose(); - if (purpose is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw) - throw new ArgumentOutOfRangeException(nameof(asset), asset, "The asset cannot be taken as a Texture2D."); + ObjectDisposedException.ThrowIf(this.isDisposed, this); - Task task; - lock (this.syncRoot) + // Check if asset is a texture asset. + if (asset.GetPurpose() is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw) { - if (this.isDisposed) - throw new ObjectDisposedException(nameof(DalamudAssetManager)); - - task = this.textureWraps[asset] ??= CreateInnerAsync(); + return Task.FromException( + new ArgumentOutOfRangeException( + nameof(asset), + asset, + "The asset does not exist or cannot be taken as a Texture2D.")); } - return task; + // Range is guaranteed to be satisfied if the asset has a purpose; get the slot for the wrap task. + ref var wrapTaskRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this.textureWraps), (int)asset); - async Task CreateInnerAsync() + // The wrap task is already set. + if (wrapTaskRef is not null) + return wrapTaskRef; + + var tcs = new TaskCompletionSource(); + if (Interlocked.CompareExchange(ref wrapTaskRef, tcs.Task, null) is not { } wrapTask) + { + // The stream task has just been set. Actually start the operation. + // In case it did not correctly finish the task in tcs, set the task to a failed state. + // Do not pass cancellation token here; we always want to touch tcs. + Task.Run( + async () => + { + try + { + tcs.SetResult(await CreateInnerAsync(this, asset)); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, + default); + return tcs.Task; + } + + // Discard the new task, and return the already created task. + tcs.SetCanceled(); + return wrapTask; + + static async Task CreateInnerAsync(DalamudAssetManager dam, DalamudAsset asset) { var buf = Array.Empty(); try { var tm = await Service.GetAsync(); - await using var stream = await this.CreateStreamAsync(asset); + await using var stream = await dam.CreateStreamAsync(asset); var length = checked((int)stream.Length); buf = ArrayPool.Shared.Rent(length); stream.ReadExactly(buf, 0, length); var name = $"{nameof(DalamudAsset)}[{Enum.GetName(asset)}]"; - var image = purpose switch + var texture = asset.GetPurpose() switch { DalamudAssetPurpose.TextureFromPng => await tm.CreateFromImageAsync(buf, name), DalamudAssetPurpose.TextureFromRaw => @@ -330,17 +384,9 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud ? await tm.CreateFromRawAsync(raw.Specification, buf, name) : throw new InvalidOperationException( "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), - _ => null, + _ => throw new InvalidOperationException(), // cannot happen }; - var disposeDeferred = - this.scopedFinalizer.Add(image) - ?? throw new InvalidOperationException("Something went wrong very badly"); - return new DisposeSuppressingTextureWrap(disposeDeferred); - } - catch (Exception e) - { - Log.Error(e, "[{name}] Failed to load {asset}.", nameof(DalamudAssetManager), asset); - throw; + return new DisposeSuppressingTextureWrap(dam.scopedFinalizer.Add(texture)); } finally { @@ -348,13 +394,4 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud } } } - - private Task TransformImmediate(Task task, Func transformer) - { - if (task.IsCompletedSuccessfully) - return Task.FromResult(transformer(task.Result)); - if (task.Exception is { } exc) - return Task.FromException(exc); - return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap(); - } } diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs index b059cb3d6..e6c7bd920 100644 --- a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs +++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs @@ -6,7 +6,7 @@ namespace Dalamud.Storage.Assets; public enum DalamudAssetPurpose { /// - /// The asset has no purpose. + /// The asset has no purpose, and is not valid and/or not accessible. /// Empty = 0,