diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 8e44942d32..838301a6b5 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/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 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().ToConstant(clipboardImpl) .Bind().ToConstant(clipboard) @@ -86,7 +48,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyboardDevice()) - .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60))) + .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer)) .Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })); @@ -100,15 +62,58 @@ namespace Avalonia.Headless /// Count of frames to be ticked on the timer. 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(); + } } } + /// + /// Options for configuring the Avalonia headless platform. + /// public class AvaloniaHeadlessPlatformOptions { + /// + /// Gets or sets the number of frames per second at which the renderer should run. + /// Default 60. + /// + public int Fps { get; set; } = 60; + + /// + /// 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. + /// + public bool ShouldRenderOnUIThread { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to use headless drawing mode, which allows rendering without creating an actual window. + /// + /// + /// Disable this option if you are using Avalonia.Skia or another drawing backend. + /// public bool UseHeadlessDrawing { get; set; } = true; + + /// + /// Gets or sets the pixel format to be used for the headless Window framebuffers. + /// public PixelFormat FrameBufferFormat { get; set; } = PixelFormat.Rgba8888; } diff --git a/src/Headless/Avalonia.Headless/HeadlessRenderTimer.cs b/src/Headless/Avalonia.Headless/HeadlessRenderTimer.cs new file mode 100644 index 0000000000..6cbbd3d425 --- /dev/null +++ b/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; + +/// +/// A render timer implementation for headless environments that uses a DispatcherTimer to schedule ticks on the UI thread. +/// Can be controlled with method. +/// +internal class HeadlessRenderTimer(int framesPerSecond) : DefaultRenderTimer(framesPerSecond) +{ + private readonly int _framesPerSecond = framesPerSecond; + private Action? _forceTick; + protected override IDisposable StartCore(Action 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(); +}