Merge pull request #1710 from goatcorp/net8-rollup

[net8] Rollup changes from master
This commit is contained in:
KazWolfe 2024-03-13 21:40:02 -07:00 committed by GitHub
commit e50f9cda99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 701 additions and 241 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -41,11 +42,13 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private readonly object runOnNextTickTaskListSync = new();
private List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private List<RunOnNextTickTaskBase> runOnNextTickTaskList2 = new();
private readonly CancellationTokenSource frameworkDestroy;
private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler;
private Thread? frameworkUpdateThread;
private readonly ConcurrentDictionary<TaskCompletionSource, (ulong Expire, CancellationToken CancellationToken)>
tickDelayedTaskCompletionSources = new();
private ulong tickCounter;
[ServiceManager.ServiceConstructor]
private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle)
@ -56,6 +59,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
this.addressResolver = new FrameworkAddressResolver();
this.addressResolver.Setup(sigScanner);
this.frameworkDestroy = new();
this.frameworkThreadTaskScheduler = new();
this.FrameworkThreadTaskFactory = new(
this.frameworkDestroy.Token,
TaskCreationOptions.None,
TaskContinuationOptions.None,
this.frameworkThreadTaskScheduler);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
@ -97,14 +108,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
/// <inheritdoc/>
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
/// <inheritdoc/>
public TaskFactory FrameworkThreadTaskFactory { get; }
/// <inheritdoc/>
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
/// <inheritdoc/>
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread;
/// <inheritdoc/>
public bool IsFrameworkUnloading { get; internal set; }
public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested;
/// <summary>
/// Gets the list of update sub-delegates that didn't get updated this frame.
@ -116,6 +130,19 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
/// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <inheritdoc/>
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
{
if (this.frameworkDestroy.IsCancellationRequested)
return Task.FromCanceled(this.frameworkDestroy.Token);
if (numTicks <= 0)
return Task.CompletedTask;
var tcs = new TaskCompletionSource();
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
return tcs.Task;
}
/// <inheritdoc/>
public Task<T> RunOnFrameworkThread<T>(Func<T> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
@ -162,20 +189,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
return Task.FromCanceled<T>(cts.Token);
}
var tcs = new TaskCompletionSource<T>();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Func = func,
});
}
return tcs.Task;
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
_ => func(),
cancellationToken);
}
/// <inheritdoc/>
@ -191,20 +214,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Action = action,
});
}
return tcs.Task;
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
_ => action(),
cancellationToken);
}
/// <inheritdoc/>
@ -220,20 +239,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
return Task.FromCanceled<T>(cts.Token);
}
var tcs = new TaskCompletionSource<Task<T>>();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task<T>>()
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Func = func,
});
}
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
_ => func(),
cancellationToken).Unwrap();
}
/// <inheritdoc/>
@ -249,20 +264,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource<Task>();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task>()
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Func = func,
});
}
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
_ => func(),
cancellationToken).Unwrap();
}
/// <summary>
@ -338,23 +349,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
}
}
private void RunPendingTickTasks()
{
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
return;
for (var i = 0; i < 2; i++)
{
lock (this.runOnNextTickTaskListSync)
(this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList);
this.runOnNextTickTaskList2.RemoveAll(x => x.Run());
}
}
private bool HandleFrameworkUpdate(IntPtr framework)
{
this.frameworkUpdateThread ??= Thread.CurrentThread;
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
ThreadSafety.MarkMainThread();
@ -388,18 +385,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
this.LastUpdate = DateTime.Now;
this.LastUpdateUTC = DateTime.UtcNow;
this.tickCounter++;
foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources)
{
if (ct.IsCancellationRequested)
k.SetCanceled(ct);
else if (expiry <= this.tickCounter)
k.SetResult();
else
continue;
this.tickDelayedTaskCompletionSources.Remove(k, out _);
}
if (StatsEnabled)
{
StatsStopwatch.Restart();
this.RunPendingTickTasks();
this.frameworkThreadTaskScheduler.Run();
StatsStopwatch.Stop();
AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds);
}
else
{
this.RunPendingTickTasks();
this.frameworkThreadTaskScheduler.Run();
}
if (StatsEnabled && this.Update != null)
@ -411,7 +420,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
// Cleanup handlers that are no longer being called
foreach (var key in this.NonUpdatedSubDelegates)
{
if (key == nameof(this.RunPendingTickTasks))
if (key == nameof(this.FrameworkThreadTaskFactory))
continue;
if (StatsHistory[key].Count > 0)
@ -438,8 +447,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
private bool HandleFrameworkDestroy(IntPtr framework)
{
this.IsFrameworkUnloading = true;
this.frameworkDestroy.Cancel();
this.DispatchUpdateEvents = false;
foreach (var k in this.tickDelayedTaskCompletionSources.Keys)
k.SetCanceled(this.frameworkDestroy.Token);
this.tickDelayedTaskCompletionSources.Clear();
// All the same, for now...
this.lifecycle.SetShuttingDown();
@ -447,95 +459,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
Log.Information("Framework::Destroy!");
Service<Dalamud>.Get().Unload();
this.RunPendingTickTasks();
this.frameworkThreadTaskScheduler.Run();
ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework);
}
private abstract class RunOnNextTickTaskBase
{
internal int RemainingTicks { get; set; }
internal long RunAfterTickCount { get; init; }
internal CancellationToken CancellationToken { get; init; }
internal bool Run()
{
if (this.CancellationToken.IsCancellationRequested)
{
this.CancelImpl();
return true;
}
if (this.RemainingTicks > 0)
this.RemainingTicks -= 1;
if (this.RemainingTicks > 0)
return false;
if (this.RunAfterTickCount > Environment.TickCount64)
return false;
this.RunImpl();
return true;
}
protected abstract void RunImpl();
protected abstract void CancelImpl();
}
private class RunOnNextTickTaskFunc<T> : RunOnNextTickTaskBase
{
internal TaskCompletionSource<T> TaskCompletionSource { get; init; }
internal Func<T> Func { get; init; }
protected override void RunImpl()
{
try
{
this.TaskCompletionSource.SetResult(this.Func());
}
catch (Exception ex)
{
this.TaskCompletionSource.SetException(ex);
}
}
protected override void CancelImpl()
{
this.TaskCompletionSource.SetCanceled();
}
}
private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
{
internal TaskCompletionSource TaskCompletionSource { get; init; }
internal Action Action { get; init; }
protected override void RunImpl()
{
try
{
this.Action();
this.TaskCompletionSource.SetResult();
}
catch (Exception ex)
{
this.TaskCompletionSource.SetException(ex);
}
}
protected override void CancelImpl()
{
this.TaskCompletionSource.SetCanceled();
}
}
}
/// <summary>
@ -586,6 +515,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
this.Update = null;
}
/// <inheritdoc/>
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) =>
this.frameworkService.DelayTicks(numTicks, cancellationToken);
/// <inheritdoc/>
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
=> this.frameworkService.RunOnFrameworkThread(func);

View file

@ -9,6 +9,7 @@ using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@ -84,11 +85,22 @@ public sealed class SingleFontChooserDialog : IDisposable
private IFontHandle? fontHandle;
private SingleFontSpec selectedFont;
/// <summary>
/// Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.
/// </summary>
private bool popupPositionChanged;
private bool popupSizeChanged;
private Vector2 popupPosition = new(float.NaN);
private Vector2 popupSize = new(float.NaN);
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="newAsyncAtlas">A new instance of <see cref="IFontAtlas"/> created using
/// <see cref="FontAtlasAutoRebuildMode.Async"/> as its auto-rebuild mode.</param>
/// <remarks>The passed instance of <see cref="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
/// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
/// this font chooser. Consider using <see cref="SingleFontChooserDialog(UiBuilder, bool, string?)"/> for automatic
/// handling of font atlas derived from a <see cref="UiBuilder"/>, or even <see cref="CreateAuto"/> for automatic
/// registration and unregistration of <see cref="Draw"/> event handler in addition to automatic disposal of this
/// class and the temporary font atlas for this font chooser dialog.</remarks>
[Obsolete("See remarks, and use the other constructor.", false)]
[Api10ToDo("Make private.")]
public SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
{
this.counter = Interlocked.Increment(ref counterStatic);
@ -99,6 +111,39 @@ public sealed class SingleFontChooserDialog : IDisposable
Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText);
}
#pragma warning disable CS0618 // Type or member is obsolete
// TODO: Api10ToDo; Remove this pragma warning disable line
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="uiBuilder">The relevant instance of UiBuilder.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <param name="debugAtlasName">Atlas name for debugging purposes.</param>
/// <remarks>
/// <para>The passed <see cref="UiBuilder"/> is only used for creating a temporary font atlas. It will not
/// automatically register a hander for <see cref="UiBuilder.Draw"/>.</para>
/// <para>Consider using <see cref="CreateAuto"/> for automatic registration and unregistration of
/// <see cref="Draw"/> event handler in addition to automatic disposal of this class and the temporary font atlas
/// for this font chooser dialog.</para>
/// </remarks>
public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null)
: this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName))
{
}
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="debugAtlasName">The temporary atlas name.</param>
internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName)
: this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async))
{
}
#pragma warning restore CS0618 // Type or member is obsolete
// TODO: Api10ToDo; Remove this pragma warning restore line
/// <summary>Called when the selected font spec has changed.</summary>
public event Action<SingleFontSpec>? SelectedFontSpecChanged;
/// <summary>
/// Gets or sets the title of this font chooser dialog popup.
/// </summary>
@ -153,6 +198,8 @@ public sealed class SingleFontChooserDialog : IDisposable
this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
this.useAdvancedOptions |= value.GlyphOffset != default;
this.useAdvancedOptions |= value.LetterSpacing != 0f;
this.SelectedFontSpecChanged?.Invoke(this.selectedFont);
}
}
@ -166,15 +213,55 @@ public sealed class SingleFontChooserDialog : IDisposable
/// </summary>
public bool IgnorePreviewGlobalScale { get; set; }
/// <summary>
/// Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and dispose itself as
/// needed.
/// <summary>Gets or sets a value indicating whether this popup should be modal, blocking everything behind from
/// being interacted.</summary>
/// <remarks>If <c>true</c>, then <see cref="ImGui.BeginPopupModal(string, ref bool, ImGuiWindowFlags)"/> will be
/// used. Otherwise, <see cref="ImGui.Begin(string, ref bool, ImGuiWindowFlags)"/> will be used.</remarks>
public bool IsModal { get; set; } = true;
/// <summary>Gets or sets the window flags.</summary>
public ImGuiWindowFlags WindowFlags { get; set; }
/// <summary>Gets or sets the popup window position.</summary>
/// <remarks>
/// <para>Setting the position only works before the first call to <see cref="Draw"/>.</para>
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default position will be used.</para>
/// <para>The position will be clamped into the work area of the selected monitor.</para>
/// </remarks>
public Vector2 PopupPosition
{
get => this.popupPosition;
set
{
this.popupPositionChanged = true;
this.popupPosition = value;
}
}
/// <summary>Gets or sets the popup window size.</summary>
/// <remarks>
/// <para>Setting the size only works before the first call to <see cref="Draw"/>.</para>
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default size will be used.</para>
/// <para>The size will be clamped into the work area of the selected monitor.</para>
/// </remarks>
public Vector2 PopupSize
{
get => this.popupSize;
set
{
this.popupSizeChanged = true;
this.popupSize = value;
}
}
/// <summary>Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and
/// dispose itself as needed; calling <see cref="Draw"/> and <see cref="Dispose"/> are handled automatically.
/// </summary>
/// <param name="uiBuilder">An instance of <see cref="UiBuilder"/>.</param>
/// <returns>The new instance of <see cref="SingleFontChooserDialog"/>.</returns>
public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
{
var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async));
var fcd = new SingleFontChooserDialog(uiBuilder);
uiBuilder.Draw += fcd.Draw;
fcd.tcs.Task.ContinueWith(
r =>
@ -187,6 +274,14 @@ public sealed class SingleFontChooserDialog : IDisposable
return fcd;
}
/// <summary>Gets the default popup size before clamping to monitor work area.</summary>
/// <returns>The default popup size.</returns>
public static Vector2 GetDefaultPopupSizeNonClamped()
{
ThreadSafety.AssertMainThread();
return new Vector2(40, 30) * ImGui.GetTextLineHeight();
}
/// <inheritdoc/>
public void Dispose()
{
@ -204,13 +299,28 @@ public sealed class SingleFontChooserDialog : IDisposable
ImGui.GetIO().WantTextInput = false;
}
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
/// being drawn.</summary>
/// <param name="preferredPopupSize">The preferred popup size.</param>
public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize)
{
ThreadSafety.AssertMainThread();
this.PopupSize = preferredPopupSize;
this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2);
}
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
/// being drawn.</summary>
public void SetPopupPositionAndSizeToCurrentWindowCenter() =>
this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped());
/// <summary>
/// Draws this dialog.
/// </summary>
public void Draw()
{
if (this.firstDraw)
ImGui.OpenPopup(this.popupImGuiName);
const float popupMinWidth = 320;
const float popupMinHeight = 240;
ImGui.GetIO().WantCaptureKeyboard = true;
ImGui.GetIO().WantTextInput = true;
@ -220,12 +330,70 @@ public sealed class SingleFontChooserDialog : IDisposable
return;
}
var open = true;
ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing);
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open)
if (this.firstDraw)
{
this.Cancel();
return;
if (this.IsModal)
ImGui.OpenPopup(this.popupImGuiName);
}
if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged)
{
var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y);
var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped();
size.X = Math.Max(size.X, popupMinWidth);
size.Y = Math.Max(size.Y, popupMinHeight);
var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y);
var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos();
var monitors = ImGui.GetPlatformIO().Monitors;
var preferredMonitor = 0;
var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]);
for (var i = 1; i < monitors.Size; i++)
{
var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]);
if (distance < preferredDistance)
{
preferredMonitor = i;
preferredDistance = distance;
}
}
var lt = monitors[preferredMonitor].WorkPos;
var workSize = monitors[preferredMonitor].WorkSize;
size.X = Math.Min(size.X, workSize.X);
size.Y = Math.Min(size.Y, workSize.Y);
var rb = (lt + workSize) - size;
var pos =
preferProvidedPos
? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y))
: (lt + rb) / 2;
ImGui.SetNextWindowSize(size, ImGuiCond.Always);
ImGui.SetNextWindowPos(pos, ImGuiCond.Always);
this.popupPositionChanged = this.popupSizeChanged = false;
}
ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue));
if (this.IsModal)
{
var open = true;
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open)
{
this.Cancel();
return;
}
}
else
{
var open = true;
if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open)
{
ImGui.End();
this.Cancel();
return;
}
}
var framePad = ImGui.GetStyle().FramePadding;
@ -261,12 +429,36 @@ public sealed class SingleFontChooserDialog : IDisposable
ImGui.EndChild();
ImGui.EndPopup();
this.popupPosition = ImGui.GetWindowPos();
this.popupSize = ImGui.GetWindowSize();
if (this.IsModal)
ImGui.EndPopup();
else
ImGui.End();
this.firstDraw = false;
this.firstDrawAfterRefresh = false;
}
private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitorPtr monitor)
{
var lt = monitor.MainPos;
var rb = monitor.MainPos + monitor.MainSize;
var xoff =
point.X < lt.X
? lt.X - point.X
: point.X > rb.X
? point.X - rb.X
: 0;
var yoff =
point.Y < lt.Y
? lt.Y - point.Y
: point.Y > rb.Y
? point.Y - rb.Y
: 0;
return MathF.Sqrt((xoff * xoff) + (yoff * yoff));
}
private void DrawChoices()
{
var lineHeight = ImGui.GetTextLineHeight();
@ -338,15 +530,20 @@ public sealed class SingleFontChooserDialog : IDisposable
}
}
if (this.IgnorePreviewGlobalScale)
if (this.fontHandle is null)
{
this.fontHandle ??= this.selectedFont.CreateFontHandle(
this.atlas,
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
}
else
{
this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas);
if (this.IgnorePreviewGlobalScale)
{
this.fontHandle = this.selectedFont.CreateFontHandle(
this.atlas,
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
}
else
{
this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas);
}
this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont);
}
if (this.fontHandle is null)

View file

@ -90,11 +90,6 @@ internal class ConsoleWindow : Window, IDisposable
this.Size = new Vector2(500, 400);
this.SizeCondition = ImGuiCond.FirstUseEver;
this.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600.0f, 200.0f),
};
this.RespectCloseHotkey = false;
this.logLinesLimit = configuration.LogLinesLimit;
@ -555,10 +550,24 @@ internal class ConsoleWindow : Window, IDisposable
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game");
ImGui.SameLine();
ImGui.SetCursorPosX(
ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X);
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
var inputWidth = 200.0f * ImGuiHelpers.GlobalScale;
var nextCursorPosX = ImGui.GetContentRegionMax().X - (2 * inputWidth) - ImGui.GetStyle().ItemSpacing.X;
var breakInputLines = nextCursorPosX < 0;
if (ImGui.GetCursorPosX() > nextCursorPosX)
{
ImGui.NewLine();
inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2);
if (!breakInputLines)
inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2;
}
else
{
ImGui.SetCursorPosX(nextCursorPosX);
}
ImGui.PushItemWidth(inputWidth);
if (ImGui.InputTextWithHint(
"##textHighlight",
"regex highlight",
@ -583,8 +592,10 @@ internal class ConsoleWindow : Window, IDisposable
log.HighlightMatches = null;
}
ImGui.SameLine();
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
if (!breakInputLines)
ImGui.SameLine();
ImGui.PushItemWidth(inputWidth);
if (ImGui.InputTextWithHint(
"##textFilter",
"regex global filter",
@ -1082,6 +1093,8 @@ internal class ConsoleWindow : Window, IDisposable
lastc = currc;
}
}
ImGui.Dummy(screenPos - ImGui.GetCursorScreenPos());
}
private record LogEntry

View file

@ -44,6 +44,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
private bool useBold;
private bool useMinimumBuild;
private SingleFontChooserDialog? chooserDialog;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; }
@ -126,32 +128,75 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
if (ImGui.Button("Test Lock"))
Task.Run(this.TestLock);
ImGui.SameLine();
if (ImGui.Button("Choose Editor Font"))
{
var fcd = new SingleFontChooserDialog(
Service<FontAtlasFactory>.Get().CreateFontAtlas(
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
FontAtlasAutoRebuildMode.Async));
fcd.SelectedFont = this.fontSpec;
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
Service<InterfaceManager>.Get().Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
fcd.Dispose();
if (this.chooserDialog is null)
{
DoNext();
}
else
{
this.chooserDialog.Cancel();
this.chooserDialog.ResultTask.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(DoNext));
this.chooserDialog = null;
}
_ = r.Exception;
if (!r.IsCompletedSuccessfully)
return;
void DoNext()
{
var fcd = new SingleFontChooserDialog(
Service<FontAtlasFactory>.Get(),
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont");
this.chooserDialog = fcd;
fcd.SelectedFont = this.fontSpec;
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
fcd.IsModal = false;
Service<InterfaceManager>.Get().Draw += fcd.Draw;
var prevSpec = this.fontSpec;
fcd.SelectedFontSpecChanged += spec =>
{
this.fontSpec = spec;
Log.Information("Selected font: {font}", this.fontSpec);
this.fontDialogHandle?.Dispose();
this.fontDialogHandle = null;
};
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
fcd.Dispose();
this.fontSpec = r.Result;
Log.Information("Selected font: {font}", this.fontSpec);
this.fontDialogHandle?.Dispose();
this.fontDialogHandle = null;
}));
_ = r.Exception;
var spec = r.IsCompletedSuccessfully ? r.Result : prevSpec;
if (this.fontSpec != spec)
{
this.fontSpec = spec;
this.fontDialogHandle?.Dispose();
this.fontDialogHandle = null;
}
this.chooserDialog = null;
}));
}
}
if (this.chooserDialog is not null)
{
ImGui.SameLine();
ImGui.TextUnformatted($"{this.chooserDialog.PopupPosition}, {this.chooserDialog.PopupSize}");
ImGui.SameLine();
if (ImGui.Button("Random Location"))
{
var monitors = ImGui.GetPlatformIO().Monitors;
var monitor = monitors[Random.Shared.Next() % monitors.Size];
this.chooserDialog.PopupPosition = monitor.WorkPos + (monitor.WorkSize * new Vector2(
Random.Shared.NextSingle(),
Random.Shared.NextSingle()));
this.chooserDialog.PopupSize = monitor.WorkSize * new Vector2(
Random.Shared.NextSingle(),
Random.Shared.NextSingle());
}
}
this.privateAtlas ??=

View file

@ -1,13 +1,22 @@
// ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions.
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
@ -18,6 +27,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class TaskSchedulerWidget : IDataWindowWidget
{
private readonly FileDialogManager fileDialogManager = new();
private readonly byte[] urlBytes = new byte[2048];
private readonly byte[] localPathBytes = new byte[2048];
private Task? downloadTask = null;
private (long Downloaded, long Total, float Percentage) downloadState;
private CancellationTokenSource taskSchedulerCancelSource = new();
/// <inheritdoc/>
@ -33,11 +48,16 @@ internal class TaskSchedulerWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
Encoding.UTF8.GetBytes(
"https://geo.mirror.pkgbuild.com/iso/2024.01.01/archlinux-2024.01.01-x86_64.iso",
this.urlBytes);
}
/// <inheritdoc/>
public void Draw()
{
var framework = Service<Framework>.Get();
if (ImGui.Button("Clear list"))
{
TaskTracker.Clear();
@ -84,8 +104,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget
{
Thread.Sleep(200);
string a = null;
a.Contains("dalamud"); // Intentional null exception.
_ = ((string)null)!.Contains("dalamud"); // Intentional null exception.
});
}
@ -94,36 +113,156 @@ internal class TaskSchedulerWidget : IDataWindowWidget
if (ImGui.Button("ASAP"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token));
_ = framework.RunOnTick(() => Log.Information("Framework.Update - ASAP"), cancellationToken: this.taskSchedulerCancelSource.Token);
}
ImGui.SameLine();
if (ImGui.Button("In 1s"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1));
}
ImGui.SameLine();
if (ImGui.Button("In 60f"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60));
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60);
}
ImGui.SameLine();
if (ImGui.Button("In 1s+120f"))
{
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s+120f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1), delayTicks: 120);
}
ImGui.SameLine();
if (ImGui.Button("In 2s+60f"))
{
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60);
}
ImGui.SameLine();
if (ImGui.Button("Every 60 frames"))
{
_ = framework.RunOnTick(
async () =>
{
for (var i = 0L; ; i++)
{
Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}");
await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token);
}
},
cancellationToken: this.taskSchedulerCancelSource.Token);
}
ImGui.SameLine();
if (ImGui.Button("Error in 1s"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
_ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1));
}
ImGui.SameLine();
if (ImGui.Button("As long as it's in Framework Thread"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); }));
Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait();
Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); }));
framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait();
}
if (ImGui.CollapsingHeader("Download"))
{
ImGui.InputText("URL", this.urlBytes, (uint)this.urlBytes.Length);
ImGui.InputText("Local Path", this.localPathBytes, (uint)this.localPathBytes.Length);
ImGui.SameLine();
if (ImGuiComponents.IconButton("##localpathpicker", FontAwesomeIcon.File))
{
var defaultFileName = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0].Split('/').Last();
this.fileDialogManager.SaveFileDialog(
"Choose a local path",
"*",
defaultFileName,
string.Empty,
(accept, newPath) =>
{
if (accept)
{
this.localPathBytes.AsSpan().Clear();
Encoding.UTF8.GetBytes(newPath, this.localPathBytes.AsSpan());
}
});
}
ImGui.TextUnformatted($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)");
using var disabled =
ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPathBytes[0] == 0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Download");
ImGui.SameLine();
var downloadUsingGlobalScheduler = ImGui.Button("using default scheduler");
ImGui.SameLine();
var downloadUsingFramework = ImGui.Button("using Framework.Update");
if (downloadUsingGlobalScheduler || downloadUsingFramework)
{
var url = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0];
var localPath = Encoding.UTF8.GetString(this.localPathBytes).Split('\0', 2)[0];
var ct = this.taskSchedulerCancelSource.Token;
this.downloadState = default;
var factory = downloadUsingGlobalScheduler
? Task.Factory
: framework.FrameworkThreadTaskFactory;
this.downloadState = default;
this.downloadTask = factory.StartNew(
async () =>
{
try
{
await using var to = File.Create(localPath);
using var client = new HttpClient();
using var conn = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
this.downloadState.Total = conn.Content.Headers.ContentLength ?? -1L;
await using var from = conn.Content.ReadAsStream(ct);
var buffer = new byte[8192];
while (true)
{
if (downloadUsingFramework)
ThreadSafety.AssertMainThread();
if (downloadUsingGlobalScheduler)
ThreadSafety.AssertNotMainThread();
var len = await from.ReadAsync(buffer, ct);
if (len == 0)
break;
await to.WriteAsync(buffer.AsMemory(0, len), ct);
this.downloadState.Downloaded += len;
if (this.downloadState.Total >= 0)
{
this.downloadState.Percentage =
(100f * this.downloadState.Downloaded) / this.downloadState.Total;
}
}
}
catch (Exception e)
{
Log.Error(e, "Failed to download {from} to {to}.", url, localPath);
try
{
File.Delete(localPath);
}
catch
{
// ignore
}
}
},
cancellationToken: ct).Unwrap();
}
}
if (ImGui.Button("Drown in tasks"))
@ -244,6 +383,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget
ImGui.PopStyleColor(1);
}
this.fileDialogManager.Draw();
}
private async Task TestTaskInTaskDelay(CancellationToken token)

View file

@ -12,7 +12,6 @@ using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@ -199,10 +198,10 @@ public class SettingsTabLook : SettingsTab
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
{
var faf = Service<FontAtlasFactory>.Get();
var fcd = new SingleFontChooserDialog(
faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
var fcd = new SingleFontChooserDialog(faf, $"{nameof(SettingsTabLook)}:Default");
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
fcd.SetPopupPositionAndSizeToCurrentWindowCenter();
interfaceManager.Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(

View file

@ -82,21 +82,25 @@ public interface IFontAtlas : IDisposable
/// </example>
public IDisposable SuppressAutoRebuild();
/// <summary>
/// Creates a new <see cref="IFontHandle"/> from game's built-in fonts.
/// </summary>
/// <summary>Creates a new <see cref="IFontHandle"/> from game's built-in fonts.</summary>
/// <param name="style">Font to use.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
/// <remarks>This function does not throw. <see cref="IFontHandle.LoadException"/> will be populated instead, if
/// the build procedure has failed. <see cref="IFontHandle.Push"/> can be used regardless of the state of the font
/// handle.</remarks>
public IFontHandle NewGameFontHandle(GameFontStyle style);
/// <summary>
/// Creates a new IFontHandle using your own callbacks.
/// </summary>
/// <summary>Creates a new IFontHandle using your own callbacks.</summary>
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
/// <remarks>
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
/// <para>Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to
/// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language
/// users.</para>
/// <para>This function does not throw, even if <paramref name="buildStepDelegate"/> would throw exceptions.
/// Instead, if it fails, the returned handle will contain an <see cref="IFontHandle.LoadException"/> property
/// containing the exception happened during the build process. <see cref="IFontHandle.Push"/> can be used even if
/// the build process has not been completed yet or failed.</para>
/// </remarks>
/// <example>
/// <b>On initialization</b>:

View file

@ -58,10 +58,27 @@ public interface IFontHandle : IDisposable
/// <returns>A disposable object that will pop the font on dispose.</returns>
/// <exception cref="InvalidOperationException">If called outside of the main thread.</exception>
/// <remarks>
/// This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
/// <para>This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
/// Use <see cref="IDisposable.Dispose"/> or <see cref="Pop"/> to undo this operation.
/// Do not use <see cref="ImGui.PopFont"/>.
/// Do not use <see cref="ImGui.PopFont"/>.</para>
/// </remarks>
/// <example>
/// <b>Push a font with `using` clause.</b>
/// <code>
/// using (fontHandle.Push())
/// ImGui.TextUnformatted("Test");
/// </code>
/// <b>Push a font with a matching call to <see cref="Pop"/>.</b>
/// <code>
/// fontHandle.Push();
/// ImGui.TextUnformatted("Test 2");
/// </code>
/// <b>Push a font between two choices.</b>
/// <code>
/// using ((someCondition ? myFontHandle : dalamudPluginInterface.UiBuilder.MonoFontHandle).Push())
/// ImGui.TextUnformatted("Test 3");
/// </code>
/// </example>
IDisposable Push();
/// <summary>

View file

@ -136,13 +136,18 @@ internal abstract class FontHandle : IFontHandle
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
/// </returns>
/// <exception cref="ObjectDisposedException">Still may be thrown.</exception>
public ILockedImFont? TryLock(out string? errorMessage)
{
IFontHandleSubstance? prevSubstance = default;
while (true)
{
var substance = this.Manager.Substance;
if (this.manager is not { } nonDisposedManager)
{
errorMessage = "The font handle has been disposed.";
return null;
}
var substance = nonDisposedManager.Substance;
// Does the associated IFontAtlas have a built substance?
if (substance is null)

View file

@ -29,6 +29,11 @@ public interface IFramework
/// </summary>
public DateTime LastUpdateUTC { get; }
/// <summary>
/// Gets a <see cref="TaskFactory"/> that runs tasks during Framework Update event.
/// </summary>
public TaskFactory FrameworkThreadTaskFactory { get; }
/// <summary>
/// Gets the delta between the last Framework Update and the currently executing one.
/// </summary>
@ -44,6 +49,14 @@ public interface IFramework
/// </summary>
public bool IsFrameworkUnloading { get; }
/// <summary>
/// Returns a task that completes after the given number of ticks.
/// </summary>
/// <param name="numTicks">Number of ticks to delay.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A new <see cref="Task"/> that gets resolved after specified number of ticks happen.</returns>
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default);
/// <summary>
/// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
/// </summary>
@ -65,6 +78,7 @@ public interface IFramework
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
[Obsolete($"Use {nameof(RunOnTick)} instead.")]
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func);
/// <summary>
@ -72,6 +86,7 @@ public interface IFramework
/// </summary>
/// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
[Obsolete($"Use {nameof(RunOnTick)} instead.")]
public Task RunOnFrameworkThread(Func<Task> func);
/// <summary>

View file

@ -75,7 +75,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask()))
.Select(x => x.ToContentDisposedTask(true)))
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
}
@ -99,6 +99,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
.Concat(this.fileStreams.Values)
.Concat(this.textureWraps.Values)
.Where(x => x is not null)
.Select(x => x.ContinueWith(r => { _ = r.Exception; }))
.ToArray());
this.scopedFinalizer.Dispose();
}

View file

@ -0,0 +1,90 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Dalamud.Utility;
/// <summary>
/// A task scheduler that runs tasks on a specific thread.
/// </summary>
internal class ThreadBoundTaskScheduler : TaskScheduler
{
private const byte Scheduled = 0;
private const byte Running = 1;
private readonly ConcurrentDictionary<Task, byte> scheduledTasks = new();
/// <summary>
/// Initializes a new instance of the <see cref="ThreadBoundTaskScheduler"/> class.
/// </summary>
/// <param name="boundThread">The thread to bind this task scheduelr to.</param>
public ThreadBoundTaskScheduler(Thread? boundThread = null)
{
this.BoundThread = boundThread;
}
/// <summary>
/// Gets or sets the thread this task scheduler is bound to.
/// </summary>
public Thread? BoundThread { get; set; }
/// <summary>
/// Gets a value indicating whether we're on the bound thread.
/// </summary>
public bool IsOnBoundThread => Thread.CurrentThread == this.BoundThread;
/// <summary>
/// Runs queued tasks.
/// </summary>
public void Run()
{
foreach (var task in this.scheduledTasks.Keys)
{
if (!this.scheduledTasks.TryUpdate(task, Running, Scheduled))
continue;
_ = this.TryExecuteTask(task);
}
}
/// <inheritdoc/>
protected override IEnumerable<Task> GetScheduledTasks()
{
return this.scheduledTasks.Keys;
}
/// <inheritdoc/>
protected override void QueueTask(Task task)
{
this.scheduledTasks[task] = Scheduled;
}
/// <inheritdoc/>
protected override bool TryDequeue(Task task)
{
if (!this.scheduledTasks.TryRemove(task, out _))
return false;
return true;
}
/// <inheritdoc/>
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
if (!this.IsOnBoundThread)
return false;
if (taskWasPreviouslyQueued && !this.scheduledTasks.TryUpdate(task, Running, Scheduled))
return false;
_ = this.TryExecuteTask(task);
return true;
}
private new bool TryExecuteTask(Task task)
{
var r = base.TryExecuteTask(task);
this.scheduledTasks.Remove(task, out _);
return r;
}
}