From 6074f9cce230cd2fb54481a73709d3b7660fd774 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Mon, 24 Aug 2026 17:09:00 +0000 Subject: [PATCH] Fix vector geometry anti-aliasing regression by making stencil buffers opt-in (#21899) * Add failing test for stencil buffer default, single-source the policy Stencil buffer selection was duplicated as `useStencilBuffers == false` across the GL, Metal and Vulkan backends, so there was no single place to review or test the default. Move it to SkiaOptions.ShouldAvoidStencilBuffers and add a test asserting the contract the default should honour. The test fails at this commit. * Avoid stencil buffers unless explicitly enabled Enabling stencil buffers lets Skia select multisample-based path rendering, which quantizes edge coverage instead of computing analytic coverage. Vector geometry therefore rasterizes with visibly softer, rougher edges on GPU backends, most noticeably on small icons. Text is unaffected because glyphs are drawn from the mask atlas rather than the path renderer. Make the null default avoid stencil buffers again, restoring 12.0.5 rendering. Applications that want the faster clipping can still opt in explicitly with SkiaOptions.UseStencilBuffers = true. Fixes #21760 --- .../Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs | 2 +- .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 2 +- .../Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs | 2 +- src/Skia/Avalonia.Skia/SkiaOptions.cs | 12 ++++++++++-- .../SkiaOptionsTests.cs | 19 +++++++++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 tests/Avalonia.Skia.UnitTests/SkiaOptionsTests.cs diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index 05bcd9fa42..c60b3b3dba 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -17,7 +17,7 @@ internal class SkiaMetalGpu : ISkiaGpu public SkiaMetalGpu(IMetalDevice device, long? maxResourceBytes, bool? useStencilBuffers = null) { - var avoidStencilBuffers = useStencilBuffers == false; + var avoidStencilBuffers = SkiaOptions.ShouldAvoidStencilBuffers(useStencilBuffers); _context = GRContext.CreateMetal( new GRMtlBackendContext { DeviceHandle = device.Device, QueueHandle = device.CommandQueue, }, new GRContextOptions { AvoidStencilBuffers = avoidStencilBuffers }) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index af957551e3..107ab79ac7 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -40,7 +40,7 @@ namespace Avalonia.Skia using(iface) { - var avoidStencilBuffers = useStencilBuffers == false; + var avoidStencilBuffers = SkiaOptions.ShouldAvoidStencilBuffers(useStencilBuffers); _grContext = GRContext.CreateGl(iface, new GRContextOptions { AvoidStencilBuffers = avoidStencilBuffers }); if (maxResourceBytes.HasValue) { diff --git a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs index b66054b340..9f8a746044 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaGpu.cs @@ -48,7 +48,7 @@ internal class VulkanSkiaGpu : ISkiaGpu GetProcedureAddress = GetProcAddressWrapper }; - var avoidStencilBuffers = useStencilBuffers == false; + var avoidStencilBuffers = SkiaOptions.ShouldAvoidStencilBuffers(useStencilBuffers); GrContext = GRContext.CreateVulkan(ctx, new GRContextOptions { AvoidStencilBuffers = avoidStencilBuffers }) ?? throw new VulkanException("Unable to create GrContext from IVulkanDevice"); diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index 1a95919300..9afaf6b307 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -26,9 +26,17 @@ namespace Avalonia public bool UseOpacitySaveLayer { get; set; } = false; /// - /// Gets whether stencil buffers can be used for various draw operations, improving performance. - /// If null (the default), Avalonia chooses whether to enable stencil buffers depending on the platform. + /// Gets whether stencil buffers can be used for various draw operations, improving clipping performance. + /// Enabling them lets Skia pick multisample-based path rendering, which quantizes edge coverage and + /// visibly degrades anti-aliasing of vector geometry, so they are disabled unless explicitly requested. /// public bool? UseStencilBuffers { get; set; } + + /// + /// Maps onto Skia's inverted GRContextOptions.AvoidStencilBuffers. + /// Only an explicit true opts in: stencil buffers trade geometry anti-aliasing quality for + /// clipping speed (see issue #21760), so they must never be enabled implicitly. + /// + internal static bool ShouldAvoidStencilBuffers(bool? useStencilBuffers) => useStencilBuffers != true; } } diff --git a/tests/Avalonia.Skia.UnitTests/SkiaOptionsTests.cs b/tests/Avalonia.Skia.UnitTests/SkiaOptionsTests.cs new file mode 100644 index 0000000000..660d89b929 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/SkiaOptionsTests.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class SkiaOptionsTests + { + // Stencil buffers let Skia choose multisample-based path rendering, which quantizes edge + // coverage and visibly degraded vector geometry anti-aliasing on GPU backends in 12.1.0. + // They are a performance opt-in, so anything other than an explicit `true` must avoid them. + [Theory] + [InlineData(null, true)] + [InlineData(false, true)] + [InlineData(true, false)] + public void Stencil_Buffers_Are_Avoided_Unless_Explicitly_Enabled(bool? useStencilBuffers, bool expected) + { + Assert.Equal(expected, SkiaOptions.ShouldAvoidStencilBuffers(useStencilBuffers)); + } + } +}