Browse Source

Add sleep timer option for the headless platform (#21423)

* Add ShouldRenderOnUIThread//Fps options to the AvaloniaHeadlessPlatformOptions, allowing SleepLoopRenderTimer to be enabled

* Don't throw from ForceRenderTimerTick

* Run DispatcherFrame on RequestCommitAsync, when HeadlessRenderTimer is not available

* This method apparently was public
pull/21537/head
Max Katz 4 months ago
committed by GitHub
parent
commit
affd240a34
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 105
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  2. 40
      src/Headless/Avalonia.Headless/HeadlessRenderTimer.cs

105
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -1,64 +1,22 @@
using System;
using System.Diagnostics;
using Avalonia.Controls.Platform;
using Avalonia.Reactive;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using System.Collections.Generic;
using Avalonia.Threading;
namespace Avalonia.Headless
{
public static class AvaloniaHeadlessPlatform
{
internal static Compositor? Compositor { get; private set; }
private static RenderTimer? s_renderTimer;
private class RenderTimer : DefaultRenderTimer
{
private readonly int _framesPerSecond;
private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
var st = Stopwatch.StartNew();
_forceTick = () => tick(st.Elapsed);
var timer = new DispatcherTimer(DispatcherPriority.UiThreadRender)
{
Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
Tag = "HeadlessRenderTimer"
};
timer.Tick += (s, e) => tick(st.Elapsed);
timer.Start();
return Disposable.Create(() =>
{
_forceTick = null;
timer.Stop();
});
}
private static IRenderTimer? s_renderTimer;
public RenderTimer(int framesPerSecond) : base(framesPerSecond)
{
_framesPerSecond = framesPerSecond;
}
public override bool RunsInBackground => false;
public void ForceTick() => _forceTick?.Invoke();
}
private class HeadlessWindowingPlatform : IWindowingPlatform
private class HeadlessWindowingPlatform(PixelFormat frameBufferFormat) : IWindowingPlatform
{
readonly PixelFormat _frameBufferFormat;
public HeadlessWindowingPlatform(PixelFormat frameBufferFormat)
{
_frameBufferFormat = frameBufferFormat;
}
public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false, _frameBufferFormat);
public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false, frameBufferFormat);
public ITopLevelImpl CreateEmbeddableTopLevel() => CreateEmbeddableWindow();
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
@ -73,12 +31,16 @@ namespace Avalonia.Headless
}
}
}
internal static void Initialize(AvaloniaHeadlessPlatformOptions opts)
{
var clipboardImpl = new HeadlessClipboardImplStub();
var clipboard = new Clipboard(clipboardImpl);
s_renderTimer = opts.ShouldRenderOnUIThread
? new HeadlessRenderTimer(opts.Fps)
: new SleepLoopRenderTimer(opts.Fps);
AvaloniaLocator.CurrentMutable
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
@ -86,7 +48,7 @@ namespace Avalonia.Headless
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderLoop>().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60)))
.Bind<IRenderLoop>().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer))
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat))
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }));
@ -100,15 +62,58 @@ namespace Avalonia.Headless
/// <param name="count">Count of frames to be ticked on the timer.</param>
public static void ForceRenderTimerTick(int count = 1)
{
for (var c = 0; c < count; c++)
s_renderTimer?.ForceTick();
if (s_renderTimer is HeadlessRenderTimer timer)
{
for (var c = 0; c < count; c++)
{
timer.ForceTick();
}
}
else
{
if (Compositor is not { } compositor)
{
throw new InvalidOperationException("Compositor is not initialized.");
}
var frame = new DispatcherFrame();
var task = compositor.RequestCommitAsync();
task.ContinueWith(static (_, s) => ((DispatcherFrame)s!).Continue = false, frame);
Dispatcher.CurrentDispatcher.PushFrame(frame);
task.GetAwaiter().GetResult();
}
}
}
/// <summary>
/// Options for configuring the Avalonia headless platform.
/// </summary>
public class AvaloniaHeadlessPlatformOptions
{
/// <summary>
/// Gets or sets the number of frames per second at which the renderer should run.
/// Default 60.
/// </summary>
public int Fps { get; set; } = 60;
/// <summary>
/// Render directly on the UI thread instead of using a dedicated render thread.
/// This can be usable if your device doesn't have multiple cores to begin with.
/// This setting is false by default.
/// </summary>
public bool ShouldRenderOnUIThread { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to use headless drawing mode, which allows rendering without creating an actual window.
/// </summary>
/// <remarks>
/// Disable this option if you are using Avalonia.Skia or another drawing backend.
/// </remarks>
public bool UseHeadlessDrawing { get; set; } = true;
/// <summary>
/// Gets or sets the pixel format to be used for the headless Window framebuffers.
/// </summary>
public PixelFormat FrameBufferFormat { get; set; } = PixelFormat.Rgba8888;
}

40
src/Headless/Avalonia.Headless/HeadlessRenderTimer.cs

@ -0,0 +1,40 @@
using System;
using System.Diagnostics;
using Avalonia.Reactive;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Headless;
/// <summary>
/// A render timer implementation for headless environments that uses a DispatcherTimer to schedule ticks on the UI thread.
/// Can be controlled with <see cref="ForceTick"/> method.
/// </summary>
internal class HeadlessRenderTimer(int framesPerSecond) : DefaultRenderTimer(framesPerSecond)
{
private readonly int _framesPerSecond = framesPerSecond;
private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
var st = Stopwatch.StartNew();
_forceTick = () => tick(st.Elapsed);
var timer = new DispatcherTimer(DispatcherPriority.UiThreadRender)
{
Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
Tag = "HeadlessRenderTimer"
};
timer.Tick += (s, e) => tick(st.Elapsed);
timer.Start();
return Disposable.Create(() =>
{
_forceTick = null;
timer.Stop();
});
}
public override bool RunsInBackground => false;
public void ForceTick() => _forceTick?.Invoke();
}
Loading…
Cancel
Save