From 7fa021e5625ca91aa22e0f2108327f08e090fb3c Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Wed, 17 Jun 2026 17:40:23 +0900 Subject: [PATCH] [macOS] Synchronize Metal drawable presentation (#21588) * Add demo app * [macOS] Present Metal drawable synchronously on main-thread resize frames * write through the out-parameter * Add ResizePattern to RenderDemo * Remove MetalResizeDemo --- native/Avalonia.Native/src/OSX/metal.mm | 33 +++++++-- samples/RenderDemo/Controls/ResizePattern.cs | 73 +++++++++++++++++++ samples/RenderDemo/MainWindow.xaml | 3 + .../RenderDemo/Pages/ResizePatternPage.xaml | 7 ++ .../Pages/ResizePatternPage.xaml.cs | 18 +++++ 5 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 samples/RenderDemo/Controls/ResizePattern.cs create mode 100644 samples/RenderDemo/Pages/ResizePatternPage.xaml create mode 100644 samples/RenderDemo/Pages/ResizePatternPage.xaml.cs diff --git a/native/Avalonia.Native/src/OSX/metal.mm b/native/Avalonia.Native/src/OSX/metal.mm index 3da1282306..05f9fe0e34 100644 --- a/native/Avalonia.Native/src/OSX/metal.mm +++ b/native/Avalonia.Native/src/OSX/metal.mm @@ -181,12 +181,13 @@ class AvnMetalRenderSession : public ComSingleObject drawable, const AvnPixelSize &size, double scaling) + AvnMetalRenderSession(AvnMetalDevice* device, CAMetalLayer* layer, id drawable, const AvnPixelSize &size, double scaling, bool presentWithTransaction) : _drawable(drawable), _size(size), _scaling(scaling), _queue(device->queue), - _texture([drawable texture]) { + _texture([drawable texture]), _presentWithTransaction(presentWithTransaction) { _layer = layer; } @@ -207,8 +208,19 @@ public: { START_ARP_CALL; auto buffer = [_queue commandBuffer]; - [buffer presentDrawable: _drawable]; - [buffer commit]; + if(_presentWithTransaction) + { + [buffer commit]; + [buffer waitUntilScheduled]; + [_drawable present]; + // Restore the default asynchronous presentation for the off-thread render loop. + _layer.presentsWithTransaction = NO; + } + else + { + [buffer presentDrawable: _drawable]; + [buffer commit]; + } } }; @@ -230,7 +242,8 @@ public: HRESULT BeginDrawing(IAvnMetalRenderingSession **ret) override { START_COM_ARP_CALL; - if([NSThread isMainThread]) + bool onMainThread = [NSThread isMainThread]; + if(onMainThread) { // Flush all existing rendering auto buffer = [_device->queue commandBuffer]; @@ -240,15 +253,21 @@ public: _scaling= PendingScaling; CGSize layerSize = {(CGFloat)_size.Width, (CGFloat)_size.Height}; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; [_layer setDrawableSize: layerSize]; + _layer.presentsWithTransaction = YES; + [CATransaction commit]; } auto drawable = [_layer nextDrawable]; if(drawable == nil) { - ret = nil; + if(onMainThread) + _layer.presentsWithTransaction = NO; + *ret = nullptr; return E_FAIL; } - *ret = new AvnMetalRenderSession(_device, _layer, drawable, _size, _scaling); + *ret = new AvnMetalRenderSession(_device, _layer, drawable, _size, _scaling, onMainThread); return 0; } }; diff --git a/samples/RenderDemo/Controls/ResizePattern.cs b/samples/RenderDemo/Controls/ResizePattern.cs new file mode 100644 index 0000000000..23a269c09d --- /dev/null +++ b/samples/RenderDemo/Controls/ResizePattern.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace RenderDemo.Controls +{ + public sealed class ResizePattern : Control + { + private static readonly IPen GridPen = new Pen(new SolidColorBrush(Color.FromRgb(60, 90, 130)), 1); + private static readonly IPen DiagonalPen = new Pen(new SolidColorBrush(Color.FromRgb(90, 90, 90)), 1); + private static readonly IPen CirclePen = new Pen(Brushes.OrangeRed, 4); + private static readonly IPen EdgePen = new Pen(Brushes.Lime, 6); + private static readonly IBrush Background = new SolidColorBrush(Color.FromRgb(16, 16, 24)); + private static readonly IBrush TextBrush = Brushes.White; + + private const double GridStep = 40; + + static ResizePattern() + { + AffectsRender(BoundsProperty); + } + + public override void Render(DrawingContext context) + { + var w = Bounds.Width; + var h = Bounds.Height; + if (w <= 0 || h <= 0) + return; + + var rect = new Rect(0, 0, w, h); + context.FillRectangle(Background, rect); + + // Grid -- uneven spacing on the far edges is the tell-tale of stretching. + for (double x = 0; x <= w; x += GridStep) + context.DrawLine(GridPen, new Point(x, 0), new Point(x, h)); + for (double y = 0; y <= h; y += GridStep) + context.DrawLine(GridPen, new Point(0, y), new Point(w, y)); + + // Corner-to-corner diagonals -- they only meet exactly at the centre + // when the aspect ratio is correct. + context.DrawLine(DiagonalPen, new Point(0, 0), new Point(w, h)); + context.DrawLine(DiagonalPen, new Point(w, 0), new Point(0, h)); + + // Edge frame -- a stretched drawable makes this peel away from the window edge. + context.DrawRectangle(null, EdgePen, rect.Deflate(3)); + + // Concentric perfect circles centred in the window. + var center = new Point(w / 2, h / 2); + var maxRadius = Math.Max(10, Math.Min(w, h) / 2 - 16); + for (var i = 1; i <= 3; i++) + { + var r = maxRadius * i / 3; + context.DrawEllipse(null, CirclePen, center, r, r); + } + + // Centre crosshair. + context.DrawLine(CirclePen, new Point(center.X - 20, center.Y), new Point(center.X + 20, center.Y)); + context.DrawLine(CirclePen, new Point(center.X, center.Y - 20), new Point(center.X, center.Y + 20)); + + // Live size read-out. + var text = new FormattedText( + string.Create(CultureInfo.InvariantCulture, $"{w:0} x {h:0} DIP"), + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + Typeface.Default, + 22, + TextBrush); + context.DrawText(text, new Point(12, 8)); + } + } +} diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index e3afd0c241..c3e68ab735 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -97,5 +97,8 @@ + + + diff --git a/samples/RenderDemo/Pages/ResizePatternPage.xaml b/samples/RenderDemo/Pages/ResizePatternPage.xaml new file mode 100644 index 0000000000..8e03879b84 --- /dev/null +++ b/samples/RenderDemo/Pages/ResizePatternPage.xaml @@ -0,0 +1,7 @@ + + + diff --git a/samples/RenderDemo/Pages/ResizePatternPage.xaml.cs b/samples/RenderDemo/Pages/ResizePatternPage.xaml.cs new file mode 100644 index 0000000000..5c7f2b7916 --- /dev/null +++ b/samples/RenderDemo/Pages/ResizePatternPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderDemo.Pages +{ + public class ResizePatternPage : UserControl + { + public ResizePatternPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +}