Skip to content

Commit

Permalink
Add TargetFrameRateOverride option
Browse files Browse the repository at this point in the history
  • Loading branch information
mayuki committed Nov 17, 2023
1 parent 7325daf commit 6309b49
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 17 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,6 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# IntelliJ IDEA / Rider
.idea/
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,24 @@ await looper.RegisterActionAsync((in LogicLooperActionContext ctx) =>

return true;
});
```
```

## Experimental
### async-aware loop actions
Experimental support for loop actions that can await asynchronous events.

With SynchronizationContext, all asynchronous continuations are executed on the loop thread.
Please beware that asynchronous actions are executed across multiple frames, unlike synchronous actions.

```csharp
await looper.RegisterActionAsync(static async (ctx, state) =>
{
state.Add("1"); // Frame: 1
await Task.Delay(250);
state.Add("2"); // Frame: 2 or later
return false;
});
```

> [!WARNING]
> If an action completes immediately (`ValueTask.IsCompleted = true`), there's no performance difference from non-async version. But it is very slow if there's a need to await. This asynchronous support provides as an emergency hatch when it becomes necessary to communicate with the outside at a low frequency. We do not recommended to perform asynchronous processing at a high frequency.
35 changes: 35 additions & 0 deletions src/LogicLooper/ILogicLooper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public interface ILogicLooper : IDisposable
/// <returns></returns>
Task RegisterActionAsync(LogicLooperActionDelegate loopAction);

/// <summary>
/// Registers a loop-frame action to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options);

/// <summary>
/// Registers a loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
Expand All @@ -40,6 +48,15 @@ public interface ILogicLooper : IDisposable
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state);

/// <summary>
/// Registers a loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action to the looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
Expand All @@ -48,6 +65,15 @@ public interface ILogicLooper : IDisposable
/// <returns></returns>
Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action to the looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
Expand All @@ -57,6 +83,15 @@ public interface ILogicLooper : IDisposable
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options);

/// <summary>
/// Stops the action loop of the looper.
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions src/LogicLooper/ILogicLooperPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ public interface ILogicLooperPool : IDisposable
/// <returns></returns>
Task RegisterActionAsync(LogicLooperActionDelegate loopAction);

/// <summary>
/// Registers a loop-frame action to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options);

/// <summary>
/// Registers a loop-frame action with state object to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
Expand All @@ -22,6 +30,15 @@ public interface ILogicLooperPool : IDisposable
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state);

/// <summary>
/// Registers a loop-frame action with state object to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
Expand All @@ -30,6 +47,15 @@ public interface ILogicLooperPool : IDisposable
/// <returns></returns>
Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action with state object to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
Expand All @@ -39,6 +65,16 @@ public interface ILogicLooperPool : IDisposable
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state);

/// <summary>
/// [Experimental] Registers an async-aware loop-frame action with state object to a pooled looper and returns <see cref="Task"/> to wait for completion.
/// An asynchronous action is executed across multiple frames, differ from the synchronous version.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <param name="options"></param>
/// <returns></returns>
Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options);

/// <summary>
/// Stops all action loop of the loopers.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/LogicLooper/Internal/NotInitializedLogicLooperPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,27 @@ internal class NotInitializedLogicLooperPool : ILogicLooperPool
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

public Task ShutdownAsync(TimeSpan shutdownDelay)
=> throw new InvalidOperationException("LogicLooper.Shared is not initialized yet.");

Expand Down
88 changes: 79 additions & 9 deletions src/LogicLooper/LogicLooper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,17 @@ public LogicLooper(TimeSpan targetFrameTime, int initialActionsCapacity = 16)
/// <param name="loopAction"></param>
/// <returns></returns>
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction)
=> RegisterActionAsync(loopAction, LooperActionOptions.Default);

/// <summary>
/// Registers a loop-frame action to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
public Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options)
{
var action = new LooperAction(DelegateHelper.GetWrapper(), loopAction, null);
var action = new LooperAction(DelegateHelper.GetWrapper(), loopAction, null, options);
return RegisterActionAsyncCore(action);
}

Expand All @@ -115,8 +124,18 @@ public Task RegisterActionAsync(LogicLooperActionDelegate loopAction)
/// <param name="state"></param>
/// <returns></returns>
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state)
=> RegisterActionAsync(loopAction, state, LooperActionOptions.Default);

/// <summary>
/// Registers a loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <param name="options"></param>
/// <returns></returns>
public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
{
var action = new LooperAction(DelegateHelper.GetWrapper<TState>(), loopAction, state);
var action = new LooperAction(DelegateHelper.GetWrapper<TState>(), loopAction, state, options);
return RegisterActionAsyncCore(action);
}

Expand All @@ -125,9 +144,18 @@ public Task RegisterActionAsync<TState>(LogicLooperActionWithStateDelegate<TStat
/// </summary>
/// <param name="loopAction"></param>
/// <returns></returns>
public async Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
=> RegisterActionAsync(loopAction, LooperActionOptions.Default);

/// <summary>
/// [Experimental] Registers a async-aware loop-frame action to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="options"></param>
/// <returns></returns>
public async Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options)
{
var action = new LooperAction(DelegateHelper.GetWrapper(), DelegateHelper.ConvertAsyncToSync(loopAction), null);
var action = new LooperAction(DelegateHelper.GetWrapper(), DelegateHelper.ConvertAsyncToSync(loopAction), null, options);
await RegisterActionAsyncCore(action);
}

Expand All @@ -137,9 +165,19 @@ public async Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction)
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <returns></returns>
public async Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state)
public Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state)
=> RegisterActionAsync<TState>(loopAction, state, LooperActionOptions.Default);

/// <summary>
/// [Experimental] Registers a async-aware loop-frame action with state object to the looper and returns <see cref="Task"/> to wait for completion.
/// </summary>
/// <param name="loopAction"></param>
/// <param name="state"></param>
/// <param name="options"></param>
/// <returns></returns>
public async Task RegisterActionAsync<TState>(LogicLooperAsyncActionWithStateDelegate<TState> loopAction, TState state, LooperActionOptions options)
{
var action = new LooperAction(DelegateHelper.GetWrapper<TState>(), DelegateHelper.ConvertAsyncToSync(loopAction), state);
var action = new LooperAction(DelegateHelper.GetWrapper<TState>(), DelegateHelper.ConvertAsyncToSync(loopAction), state, options);
await RegisterActionAsyncCore(action);
}

Expand Down Expand Up @@ -222,7 +260,7 @@ private void RunLoop()
var elapsedTimeFromPreviousFrame = TimeSpan.FromTicks(elapsedTicksFromPreviousFrame);
lastTimestamp = begin;

var ctx = new LogicLooperActionContext(this, _frame++, elapsedTimeFromPreviousFrame, _ctsAction.Token);
var ctx = new LogicLooperActionContext(this, _frame++, begin, elapsedTimeFromPreviousFrame, _ctsAction.Token);

var j = _tail - 1;
for (var i = 0; i < _actions.Length; i++)
Expand Down Expand Up @@ -303,7 +341,19 @@ private static bool InvokeAction(in LogicLooperActionContext ctx, ref LooperActi
{
try
{
// If the action haven't reached the next invoke timestamp, we don't need to perform the action.
if (action.NextScheduledTimestamp is { } nextScheduledTimestamp && nextScheduledTimestamp > ctx.FrameBeginTimestamp)
{
return true;
}

var hasNext = action.Invoke(ctx);

if (action.TargetFrameTimeTimestampOverride is { } targetFrameTimeTimestampOverride)
{
action.NextScheduledTimestamp = ctx.FrameBeginTimestamp + targetFrameTimeTimestampOverride;
}

if (!hasNext)
{
action.Future.SetResult(true);
Expand Down Expand Up @@ -376,21 +426,31 @@ static class Cache<T>

internal delegate bool InternalLogicLooperActionDelegate(object wrappedDelegate, in LogicLooperActionContext ctx, object? state);

internal readonly struct LooperAction
internal struct LooperAction
{
public DateTimeOffset BeginAt { get; }
public object? State { get; }
public Delegate? Action { get; }
public InternalLogicLooperActionDelegate ActionWrapper { get; }
public TaskCompletionSource<bool> Future { get; }
public LooperActionOptions Options { get; }

public long? NextScheduledTimestamp { get; set; }
public long? TargetFrameTimeTimestampOverride { get; }

public LooperAction(InternalLogicLooperActionDelegate actionWrapper, Delegate action, object? state)
public LooperAction(InternalLogicLooperActionDelegate actionWrapper, Delegate action, object? state, LooperActionOptions options)
{
BeginAt = DateTimeOffset.Now;
ActionWrapper = actionWrapper ?? throw new ArgumentNullException(nameof(actionWrapper));
Action = action ?? throw new ArgumentNullException(nameof(action));
State = state;
Future = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Options = options;

if (options.TargetFrameRateOverride is { } targetFrameRateOverride)
{
TargetFrameTimeTimestampOverride = (long)(TimeSpan.FromMilliseconds(1000 / (double)targetFrameRateOverride).Ticks / TimestampsToTicks);
}
}

public bool Invoke(in LogicLooperActionContext ctx)
Expand All @@ -399,3 +459,13 @@ public bool Invoke(in LogicLooperActionContext ctx)
}
}
}

public record LooperActionOptions(int? TargetFrameRateOverride = null)
{
public static LooperActionOptions Default { get; } = new LooperActionOptions();

public TimeSpan? TargetFrameTimeOverride { get; } =
TargetFrameRateOverride is {} targetOverrideFrameTime
? TimeSpan.FromMilliseconds(1000 / (double)TargetFrameRateOverride)
: null;
}
8 changes: 7 additions & 1 deletion src/LogicLooper/LogicLooperActionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public readonly struct LogicLooperActionContext
/// Gets a current frame that elapsed since beginning the looper is started.
/// </summary>
public long CurrentFrame { get; }

/// <summary>
/// Gets a timestamp for begin of the current frame.
/// </summary>
public long FrameBeginTimestamp { get; }

/// <summary>
/// Gets an elapsed time since the previous frame has proceeded. This is the equivalent to Time.deltaTime on Unity.
Expand All @@ -25,10 +30,11 @@ public readonly struct LogicLooperActionContext
/// </summary>
public CancellationToken CancellationToken { get; }

public LogicLooperActionContext(ILogicLooper looper, long currentFrame, TimeSpan elapsedTimeFromPreviousFrame, CancellationToken cancellationToken)
public LogicLooperActionContext(ILogicLooper looper, long currentFrame, long frameBeginTimestamp, TimeSpan elapsedTimeFromPreviousFrame, CancellationToken cancellationToken)
{
Looper = looper ?? throw new ArgumentNullException(nameof(looper));
CurrentFrame = currentFrame;
FrameBeginTimestamp = frameBeginTimestamp;
ElapsedTimeFromPreviousFrame = elapsedTimeFromPreviousFrame;
CancellationToken = cancellationToken;
}
Expand Down
Loading

0 comments on commit 6309b49

Please sign in to comment.