Skip to content

Commit

Permalink
Make UiThreadRenderTimer to calculate the next tick time based on exp…
Browse files Browse the repository at this point in the history
…ected FPS
  • Loading branch information
kekekeks committed Sep 25, 2024
1 parent 2299d96 commit dcaceb1
Showing 1 changed file with 57 additions and 23 deletions.
80 changes: 57 additions & 23 deletions src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,73 @@
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Threading;
using System;
using System.Diagnostics;
using Avalonia.Metadata;
using Avalonia.Threading;

namespace Avalonia.Rendering
namespace Avalonia.Rendering;

/// <summary>
/// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms
/// </summary>
public class UiThreadRenderTimer : DefaultRenderTimer
{
private readonly Stopwatch _clock = Stopwatch.StartNew();
/// <summary>
/// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms
/// Initializes a new instance of the <see cref="UiThreadRenderTimer"/> class.
/// </summary>
[PrivateApi]
public class UiThreadRenderTimer : DefaultRenderTimer
/// <param name="framesPerSecond">The number of frames per second at which the loop should run.</param>
public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
{
}

/// <inheritdoc />
public override bool RunsInBackground => false;

class TimerInstance : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="UiThreadRenderTimer"/> class.
/// </summary>
/// <param name="framesPerSecond">The number of frames per second at which the loop should run.</param>
public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
private UiThreadRenderTimer _parent;
private readonly Action<TimeSpan> _tick;
private DispatcherTimer _timer = new DispatcherTimer(DispatcherPriority.Render);

public TimerInstance(UiThreadRenderTimer parent, Action<TimeSpan> tick)
{
_parent = parent;
_tick = tick;
_timer.Tick += OnTick;
_timer.Interval = Interval;
_timer.Start();
}

/// <inheritdoc />
public override bool RunsInBackground => false;

/// <inheritdoc />
protected override IDisposable StartCore(Action<TimeSpan> tick)
private void OnTick(object? sender, EventArgs e)
{
bool cancelled = false;
var st = Stopwatch.StartNew();
DispatcherTimer.Run(() =>
var tickedAt = _parent._clock.Elapsed;
var nextTickAt = tickedAt + Interval;
try
{
if (cancelled)
return false;
tick(st.Elapsed);
return !cancelled;
}, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.UiThreadRender);
return Disposable.Create(() => cancelled = true);
_tick?.Invoke(tickedAt);
}
finally
{
var afterTick = _parent._clock.Elapsed;
var interval = nextTickAt - afterTick;
if (interval < s_minInterval)
// We are way overdue, but shouldn't cause starvation in other areas
interval = s_minInterval;
_timer.Interval = interval;
}
}

private static readonly TimeSpan s_minInterval = TimeSpan.FromMilliseconds(1);
private TimeSpan Interval => TimeSpan.FromSeconds(1.0 / _parent.FramesPerSecond);

public void Dispose() => _timer.Stop();
}

/// <inheritdoc />
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
return new TimerInstance(this, tick);
}
}

0 comments on commit dcaceb1

Please sign in to comment.