mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Add Service<Framework>.RunOnTick() (#832)
This commit is contained in:
parent
bf1a525e4c
commit
9413755ee3
2 changed files with 237 additions and 6 deletions
|
|
@ -4,7 +4,7 @@ using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Game.Gui;
|
using Dalamud.Game.Gui;
|
||||||
using Dalamud.Game.Gui.Toast;
|
using Dalamud.Game.Gui.Toast;
|
||||||
using Dalamud.Game.Libc;
|
using Dalamud.Game.Libc;
|
||||||
|
|
@ -26,7 +26,9 @@ namespace Dalamud.Game
|
||||||
public sealed class Framework : IDisposable
|
public sealed class Framework : IDisposable
|
||||||
{
|
{
|
||||||
private static Stopwatch statsStopwatch = new();
|
private static Stopwatch statsStopwatch = new();
|
||||||
private Stopwatch updateStopwatch = new();
|
|
||||||
|
private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
|
||||||
|
private readonly Stopwatch updateStopwatch = new();
|
||||||
|
|
||||||
private bool tier2Initialized = false;
|
private bool tier2Initialized = false;
|
||||||
private bool tier3Initialized = false;
|
private bool tier3Initialized = false;
|
||||||
|
|
@ -36,6 +38,8 @@ namespace Dalamud.Game
|
||||||
private Hook<OnDestroyDetour> destroyHook;
|
private Hook<OnDestroyDetour> destroyHook;
|
||||||
private Hook<OnRealDestroyDelegate> realDestroyHook;
|
private Hook<OnRealDestroyDelegate> realDestroyHook;
|
||||||
|
|
||||||
|
private Thread? frameworkUpdateThread;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Framework"/> class.
|
/// Initializes a new instance of the <see cref="Framework"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -113,6 +117,11 @@ namespace Dalamud.Game
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
|
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether currently executing code is running in the game's framework update thread.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether to dispatch update events.
|
/// Gets or sets a value indicating whether to dispatch update events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -132,6 +141,85 @@ namespace Dalamud.Game
|
||||||
this.realDestroyHook.Enable();
|
this.realDestroyHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <typeparam name="T">Return type.</typeparam>
|
||||||
|
/// <param name="func">Function to call.</param>
|
||||||
|
/// <returns>Task representing the pending or already completed function.</returns>
|
||||||
|
public Task<T> RunOnFrameworkThread<T>(Func<T> func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <param name="action">Function to call.</param>
|
||||||
|
/// <returns>Task representing the pending or already completed function.</returns>
|
||||||
|
public Task RunOnFrameworkThread(Action action)
|
||||||
|
{
|
||||||
|
if (this.IsInFrameworkUpdateThread)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Task.FromException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this.RunOnTick(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run given function in upcoming Framework.Tick call.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Return type.</typeparam>
|
||||||
|
/// <param name="func">Function to call.</param>
|
||||||
|
/// <param name="delay">Wait for given timespan before calling this function.</param>
|
||||||
|
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
|
||||||
|
/// <returns>Task representing the pending function.</returns>
|
||||||
|
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<T>();
|
||||||
|
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
|
||||||
|
{
|
||||||
|
RemainingTicks = delayTicks,
|
||||||
|
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
TaskCompletionSource = tcs,
|
||||||
|
Func = func,
|
||||||
|
});
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run given function in upcoming Framework.Tick call.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Return type.</typeparam>
|
||||||
|
/// <param name="action">Function to call.</param>
|
||||||
|
/// <param name="delay">Wait for given timespan before calling this function.</param>
|
||||||
|
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
|
||||||
|
/// <returns>Task representing the pending function.</returns>
|
||||||
|
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource();
|
||||||
|
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
|
||||||
|
{
|
||||||
|
RemainingTicks = delayTicks,
|
||||||
|
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
TaskCompletionSource = tcs,
|
||||||
|
Action = action,
|
||||||
|
});
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose of managed and unmanaged resources.
|
/// Dispose of managed and unmanaged resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -179,6 +267,8 @@ namespace Dalamud.Game
|
||||||
if (this.tierInitError)
|
if (this.tierInitError)
|
||||||
goto original;
|
goto original;
|
||||||
|
|
||||||
|
this.frameworkUpdateThread ??= Thread.CurrentThread;
|
||||||
|
|
||||||
var dalamud = Service<Dalamud>.Get();
|
var dalamud = Service<Dalamud>.Get();
|
||||||
|
|
||||||
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
|
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
|
||||||
|
|
@ -223,6 +313,8 @@ namespace Dalamud.Game
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
this.runOnNextTickTaskList.RemoveAll(x => x.Run());
|
||||||
|
|
||||||
if (StatsEnabled && this.Update != null)
|
if (StatsEnabled && this.Update != null)
|
||||||
{
|
{
|
||||||
// Stat Tracking for Framework Updates
|
// Stat Tracking for Framework Updates
|
||||||
|
|
@ -312,5 +404,88 @@ namespace Dalamud.Game
|
||||||
// Return the original trampoline location to cleanly exit
|
// Return the original trampoline location to cleanly exit
|
||||||
return originalPtr;
|
return originalPtr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,9 @@ namespace Dalamud.Interface.Internal.Windows
|
||||||
private DtrBarEntry? dtrTest2;
|
private DtrBarEntry? dtrTest2;
|
||||||
private DtrBarEntry? dtrTest3;
|
private DtrBarEntry? dtrTest3;
|
||||||
|
|
||||||
|
// Task Scheduler
|
||||||
|
private CancellationTokenSource taskSchedCancelSource = new();
|
||||||
|
|
||||||
private uint copyButtonIndex = 0;
|
private uint copyButtonIndex = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1359,6 +1362,15 @@ namespace Dalamud.Interface.Internal.Windows
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("Cancel using CancellationTokenSource"))
|
||||||
|
{
|
||||||
|
this.taskSchedCancelSource.Cancel();
|
||||||
|
this.taskSchedCancelSource = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text("Run in any thread: ");
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (ImGui.Button("Short Task.Run"))
|
if (ImGui.Button("Short Task.Run"))
|
||||||
{
|
{
|
||||||
Task.Run(() => { Thread.Sleep(500); });
|
Task.Run(() => { Thread.Sleep(500); });
|
||||||
|
|
@ -1368,7 +1380,8 @@ namespace Dalamud.Interface.Internal.Windows
|
||||||
|
|
||||||
if (ImGui.Button("Task in task(Delay)"))
|
if (ImGui.Button("Task in task(Delay)"))
|
||||||
{
|
{
|
||||||
Task.Run(async () => await this.TestTaskInTaskDelay());
|
var token = this.taskSchedCancelSource.Token;
|
||||||
|
Task.Run(async () => await this.TestTaskInTaskDelay(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
@ -1391,28 +1404,71 @@ namespace Dalamud.Interface.Internal.Windows
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Text("Run in Framework.Update: ");
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("ASAP"))
|
||||||
|
{
|
||||||
|
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("In 1s"))
|
||||||
|
{
|
||||||
|
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("In 60f"))
|
||||||
|
{
|
||||||
|
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delayTicks: 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("Error in 1s"))
|
||||||
|
{
|
||||||
|
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedCancelSource.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();
|
||||||
|
}
|
||||||
|
|
||||||
if (ImGui.Button("Drown in tasks"))
|
if (ImGui.Button("Drown in tasks"))
|
||||||
{
|
{
|
||||||
|
var token = this.taskSchedCancelSource.Token;
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
token.ThrowIfCancellationRequested();
|
||||||
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1652,9 +1708,9 @@ namespace Dalamud.Interface.Internal.Windows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TestTaskInTaskDelay()
|
private async Task TestTaskInTaskDelay(CancellationToken token)
|
||||||
{
|
{
|
||||||
await Task.Delay(5000);
|
await Task.Delay(5000, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable 1998
|
#pragma warning disable 1998
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue