From 12e0ef863229834393f81932ac256c3429d21cf8 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:24:23 +0300 Subject: [PATCH] Fix Metal renderer memory leakage on macOS (#21099) * Fix Metal renderer memory leakage on macOS * Remove no-op ResetContext call * Disposable wrapper * Success flag in ctor to dispose * Rename variable name to _autoReleasePool * Abstraction over AutoReleasePool * Use Interlocked.Exchange in AutoReleasePool --------- Co-authored-by: Julien Lebosquain --- .../Gpu/Metal/AutoReleasePool.cs | 30 ++++++++++++++++ .../Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs | 36 +++++++++++++++---- 2 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Gpu/Metal/AutoReleasePool.cs diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/AutoReleasePool.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/AutoReleasePool.cs new file mode 100644 index 0000000000..76aa09b603 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/AutoReleasePool.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Avalonia.Skia.Metal; + +internal sealed partial class AutoReleasePool : IDisposable +{ + private IntPtr _pool; + + public AutoReleasePool() + { + _pool = Push(); + } + + public void Dispose() + { + var pool = Interlocked.Exchange(ref _pool, IntPtr.Zero); + if (pool != IntPtr.Zero) + { + Pop(pool); + } + } + + [LibraryImport("libobjc", EntryPoint = "objc_autoreleasePoolPush")] + private static partial IntPtr Push(); + + [LibraryImport("libobjc", EntryPoint = "objc_autoreleasePoolPop")] + private static partial void Pop(IntPtr pool); +} diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index 402d83fd10..ccb3a51705 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -97,15 +97,33 @@ internal class SkiaMetalGpu : ISkiaGpu public ISkiaGpuRenderSession BeginRenderingSession(IRenderTarget.RenderTargetSceneInfo sceneInfo) { // TODO: use expectedPixelSize - var session = (_target ?? throw new ObjectDisposedException(nameof(SkiaMetalRenderTarget))).BeginRendering(); - var backendTarget = new GRBackendRenderTarget(session.Size.Width, session.Size.Height, - new GRMtlTextureInfo(session.Texture)); + IMetalPlatformSurfaceRenderingSession? session = null; + GRBackendRenderTarget? backendTarget = null; + SKSurface? surface = null; + var success = false; + try + { + session = (_target ?? throw new ObjectDisposedException(nameof(SkiaMetalRenderTarget))).BeginRendering(); + backendTarget = new GRBackendRenderTarget(session.Size.Width, session.Size.Height, + new GRMtlTextureInfo(session.Texture)); - var surface = SKSurface.Create(_gpu._context!, backendTarget, - session.IsYFlipped ? GRSurfaceOrigin.BottomLeft : GRSurfaceOrigin.TopLeft, - SKColorType.Bgra8888); + surface = SKSurface.Create(_gpu._context!, backendTarget, + session.IsYFlipped ? GRSurfaceOrigin.BottomLeft : GRSurfaceOrigin.TopLeft, + SKColorType.Bgra8888); - return new SkiaMetalRenderSession(_gpu, surface, session, backendTarget); + var result = new SkiaMetalRenderSession(_gpu, surface, session, backendTarget); + success = true; + return result; + } + finally + { + if (!success) + { + surface?.Dispose(); + backendTarget?.Dispose(); + session?.Dispose(); + } + } } public PlatformRenderTargetState State => _target?.State ?? PlatformRenderTargetState.Disposed; @@ -117,12 +135,14 @@ internal class SkiaMetalGpu : ISkiaGpu private SKSurface? _surface; private IMetalPlatformSurfaceRenderingSession? _session; private GRBackendRenderTarget? _backendTarget; + private readonly AutoReleasePool _autoReleasePool; public SkiaMetalRenderSession(SkiaMetalGpu gpu, SKSurface surface, IMetalPlatformSurfaceRenderingSession session, GRBackendRenderTarget backendTarget) { + _autoReleasePool = new AutoReleasePool(); _gpu = gpu; _surface = surface; _session = session; @@ -141,6 +161,8 @@ internal class SkiaMetalGpu : ISkiaGpu _session = null; _backendTarget?.Dispose(); _backendTarget = null; + + _autoReleasePool.Dispose(); } public GRContext GrContext => _gpu._context!;